<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yul.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 24 Nov 2025 02:53:13 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>yul.log</title>
            <url>https://velog.velcdn.com/images/yul_ee/profile/18cc3d87-0e91-4233-a184-99e4a7fb5ebb/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. yul.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yul_ee" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[백준/Python] 17298번: 오큰수]]></title>
            <link>https://velog.io/@yul_ee/%EB%B0%B1%EC%A4%80Python-17298%EB%B2%88-%EC%98%A4%ED%81%B0%EC%88%98</link>
            <guid>https://velog.io/@yul_ee/%EB%B0%B1%EC%A4%80Python-17298%EB%B2%88-%EC%98%A4%ED%81%B0%EC%88%98</guid>
            <pubDate>Mon, 24 Nov 2025 02:53:13 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/17298">https://www.acmicpc.net/problem/17298</a></p>
<blockquote>
<p>크기가 N인 수열 A = A1, A2, ..., AN이 있다. 수열의 각 원소 Ai에 대해서 오큰수 NGE(i)를 구하려고 한다. Ai의 오큰수는 오른쪽에 있으면서 Ai보다 큰 수 중에서 가장 왼쪽에 있는 수를 의미한다. 그러한 수가 없는 경우에 오큰수는 -1이다.
예를 들어, A = [3, 5, 2, 7]인 경우 NGE(1) = 5, NGE(2) = 7, NGE(3) = 7, NGE(4) = -1이다. A = [9, 5, 4, 8]인 경우에는 NGE(1) = -1, NGE(2) = 8, NGE(3) = 8, NGE(4) = -1이다.</p>
</blockquote>
<br>

<h2 id="풀이과정">풀이과정</h2>
<p>N의 최대 크기가 1,000,000이므로, 단순히 이중 for문으로 오른쪽 값을 확인하면 시간 초과가 발생한다. 따라서 한 번의 순회로 오큰수를 찾기 위해 <code>스택</code>을 활용한다.</p>
<p>핵심 아이디어는 다음과 같다.</p>
<ol>
<li>스택에는 아직 오큰수를 찾지 못한 인덱스를 저장한다.</li>
<li>i번째 원소 lst[i]를 보면서,
스택 top에 있는 인덱스들의 값이 lst[i]보다 작다면
해당 값들의 오큰수는 lst[i]이므로 answer에 기록하고 스택에서 제거한다.</li>
<li>i는 아직 오큰수를 찾지 못한 상태이므로 스택에 push한다.</li>
</ol>
<p>이 과정을 전체 배열에 대해 수행하면 각 원소의 오큰수를 O(N)에 구할 수 있다.</p>
<br>

<h2 id="풀이">풀이</h2>
<pre><code class="language-python">n = int(input())
lst = list(map(int, input().split()))
answer = [-1] * n

stack = [0] # 스택에는 아직 오큰수를 찾지 못한 수의 인덱스가 들어있다!
for i in range(1, n):
    while stack and lst[stack[-1]] &lt; lst[i]:
        idx = stack.pop()
        answer[idx] = lst[i]
    stack.append(i)

print(*answer)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준/Python] 1377번: 버블 소트]]></title>
            <link>https://velog.io/@yul_ee/%EB%B0%B1%EC%A4%80Python-1377%EB%B2%88-%EB%B2%84%EB%B8%94-%EC%86%8C%ED%8A%B8</link>
            <guid>https://velog.io/@yul_ee/%EB%B0%B1%EC%A4%80Python-1377%EB%B2%88-%EB%B2%84%EB%B8%94-%EC%86%8C%ED%8A%B8</guid>
            <pubDate>Sun, 23 Nov 2025 04:24:35 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/1377">https://www.acmicpc.net/problem/1377</a></p>
<p>(요약) 아래 소스코드를 실행했을 때, 어떤 값이 출력되는지 맞추는 문제</p>
<pre><code class="language-cpp">bool changed = false;
for (int i=1; i&lt;=N+1; i++) {
    changed = false;
    for (int j=1; j&lt;=N-i; j++) {
        if (A[j] &gt; A[j+1]) {
            changed = true;
            swap(A[j], A[j+1]);
        }
    }
    if (changed == false) {
        cout &lt;&lt; i &lt;&lt; &#39;\n&#39;;
        break;
    }
}</code></pre>
<br>

<h2 id="풀이과정">풀이과정</h2>
<p>&quot;N은 500,000보다 작거나 같은 자연수이다.&quot; 라는 조건이 있으므로, 시간제한이 1초인 해당 문제를 <code>버블 정렬</code>(최악의 경우, 시간복잡도 <code>O(N^2)</code>)으로는 풀 수 없다. </p>
<p>따라서 처음에는 <code>O(N log N)</code>의 시간복잡도를 갖는 <code>머지 소트</code>를 구현해서 정렬 후의 idx가 정렬 전의 idx와 일치하는 경우를 찾아 출력하는 방식을 고려하였다. 하지만 직접 시뮬레이션 하는 것보다 더 좋은 방법이 있음을 알게 되어 해당 아이디어를 공유한다. </p>
<pre><code class="language-cpp">bool changed = false;
for (int i=1; i&lt;=N+1; i++) {
    changed = false;
    for (int j=1; j&lt;=N-i; j++) {
        if (A[j] &gt; A[j+1]) {
            changed = true;
            swap(A[j], A[j+1]);
        }
    }
    if (changed == false) {
        cout &lt;&lt; i &lt;&lt; &#39;\n&#39;;
        break;
    }
}</code></pre>
<p>출력해야 하는 수 <code>i</code> 가 의미하는 것은 뭘까?
더 이상 changed가 일어나지 않는, 즉 <strong>이미 정렬된 상태에 이르렀음</strong>을 의미한다. 따라서 몇번째 패스(<code>i</code>) 직전에 이미 정렬이 완료되었는지를 구하면 되는 문제라고 이해할 수 있다.</p>
<p>이때 버블 정렬의 경우, 각 원소는 한 번에 한 칸씩만 이동(swap)할 수 있다. 이 특성을 활용하면 <strong>정렬 전 원소의 위치와 정렬 후 원소의 위치를 비교해서 가장 많은 칸을 이동한 원소의 칸 수</strong>를 통해, 몇 번의 패스동안 유의미한 정렬이 이루어졌는지를 파악할 수 있게 된다.</p>
<h2 id="풀이">풀이</h2>
<pre><code class="language-python">n = int(input())
lst = []

for i in range(n):
    num = int(input())
    lst.append((num, i))
sorted_lst = sorted(lst)

answer = 0
for i in range(n):
    answer = max(answer, sorted_lst[i][1] - i)

print(answer+1)</code></pre>
<p>정렬 전 <code>lst</code>를 (num, <code>정렬 전 idx</code>) 형태의 튜플로 저장한다. 
이후 정렬된 <code>sorted_lst</code>의 <code>idx</code>와 <code>정렬 전 idx</code>의 차이의 최대값을 구한다.
이 값이 곧, 최대 몇 번의 Pass동안 유의미한 정렬이 이루어졌는지를 의미한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[토비의 스프링 3.1] 4주차 스터디 - 예외]]></title>
            <link>https://velog.io/@yul_ee/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-3.1-4%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-%EC%98%88%EC%99%B8</link>
            <guid>https://velog.io/@yul_ee/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-3.1-4%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-%EC%98%88%EC%99%B8</guid>
            <pubDate>Fri, 24 Jan 2025 06:55:39 GMT</pubDate>
            <description><![CDATA[<p><code>스터디 날짜</code> 24.07.29.
<code>스터디 범위</code> 4장. 예외</p>
<hr>
<h2 id="412-예외의-종류와-특징">4.1.2 예외의 종류와 특징</h2>
<h3 id="자바에서-throw를-통해-발생시킬-수-있는-예외"><strong>자바에서 throw를 통해 발생시킬 수 있는 예외</strong></h3>
<ol>
<li><strong>Error</strong><ol>
<li>자바 VM에서 발생시키며, 애플리케이션 레벨에서 잡을 수 있는 예외가 아니다.</li>
<li>ex) OutOfMemory, ThreadDeath</li>
</ol>
</li>
<li><strong>Exception과 체크 예외</strong><ol>
<li>애플리케이션 코드 상에 예외사항이 발생했을 때 <code>java.lang.Exception</code> 클래스가 사용된다. </li>
<li>Exception 클래스는 체크 예외와 언체크 예외로 구분된다. <img width="381" alt="Untitled" src="https://github.com/user-attachments/assets/a972c40c-9d8a-4ff7-8be5-0ab7d514abac" />


</li>
</ol>
</li>
</ol>
<pre><code>3. 일반적으로 예외는 체크 예외라고 생각하면 된다. 

    → ex) IOException, SQLException

4. 체크 예외가 발생할 수 있는 메서드를 사용할 때는 반드시 예외 처리 코드를 함께 작성해야 한다. 

    → 체크 예외는 복구할 가능성이 조금이라도 있는, 예외사항을 다루기 때문.</code></pre><ol start="3">
<li><p><strong>RuntimeException과 언체크/런타임 예외</strong></p>
<ol>
<li><p>명시적인 예외 처리를 강제하지 않으며, 프로그램 오류가 있을 때 발생되도록 의도된 것들이다. </p>
<p> → ex) NULLPointerException, IllegalArgumentException</p>
</li>
<li><p>런타임 예외는 예상치 못했던 상황에서 발생하는 것이 아니고, 피할 수 있지만 <strong>개발자의 부주의</strong>에 의해 발생할 수 있는 예외를 다룬다. </p>
</li>
</ol>
</li>
</ol>
<p>→ 체크 예외는 예외 처리를 강제하는 것 때문에 예외 블랙홀이나 무책임한 throws 코드가 남발되어 비난의 대상이 되기도 했다. </p>
<Br>

<h2 id="413-예외처리-방법">4.1.3 예외처리 방법</h2>
<h3 id="예외를-처리하는-일반적인-방법">예외를 처리하는 일반적인 방법</h3>
<ol>
<li><p>예외 복구</p>
<ul>
<li>예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것</li>
<li>기본 작업 흐름이 불가능할 때 다른 작업 흐름으로 자연스럽게 유도해주는 경우 예외를 복구했다고 볼 수 있다.</li>
</ul>
</li>
<li><p>예외처리 회피</p>
<ul>
<li><p>자신을 호출한 쪽으로 예외처리를 던져버린다.</p>
<p>→ thorws 문으로 선언 or catch 문으로 예외를 잡고 rethrow</p>
<pre><code class="language-java">public void add() throws SQLException {
  try {
      //
  } catch(SQLException e) {
      // pring log
      throw e;
  }
}</code></pre>
</li>
<li><p>예외를 회피하는 것은 예외를 복구하는 것처럼 의도가 분명해야 한다.</p>
</li>
<li><p>콜백/탬플릿처럼 긴밀한 관계에 있는 다른 오브젝트에게 책임을 분명히 지게 하거나,</p>
</li>
<li><p>자신을 사용하는 쪽에서 예외를 다루는 게 최선의 방법이라는 분명한 확신이 있어야 한다.</p>
</li>
</ul>
</li>
<li><p>예외 전환</p>
<ul>
<li><p>예외 회피와 달리, 발생한 예외를 그대로 넘기는 것이 아니라 적절한 예외로 전환해서 던진다.</p>
</li>
<li><p>의미를 분명하게 해줄 수 있는 예외로 바꿔줄 수 있다.</p>
<ul>
<li><p>중복 아이디 사용자를 등록하려고 할 때 SQLException → DuplicationUserIdException</p>
<pre><code class="language-java">public void add(User user) thorws DuplicateUserIdException, SQLException {
  try {
      // DB에 User 추가
  } catch(SQLException e) {
      if (e.getErrorCode() == MysqlErrorNubmers.ER_DUP_ENTRY)
          throw DuplicateUserIdException();
      else
          throw e;
      }
  }</code></pre>
</li>
</ul>
</li>
<li><p>전환하는 예외에 원래 발생한 예외를 담아서 중첩예외로 만드는 것이 좋다.</p>
<pre><code class="language-java">  catch(SQLException e) {
      throw DuplicateUserIdException(e);
  }

  catch(SQLException e) {
      throw DuplicateUserIdException().initCause(e);
  }</code></pre>
<ul>
<li>의미를 명확하게 하려고 예외 전환하는 것이 아니고, <strong>예외처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸는 경우에 사용한다.</strong></li>
<li>복구 불가능한 예외라면 가능한 한 빨리 런타임 예외로 포장해 던지게 해서 다른 계층의 메소드를 작성할 때 불필요한 thorws 선언이 들어가지 않도록 해줘야 한다.</li>
</ul>
</li>
</ul>
</li>
</ol>
<Br>

<h2 id="414-예외처리-전략">4.1.4 예외처리 전략</h2>
<h3 id="런타임-예외의-보편화">런타임 예외의 보편화</h3>
<ul>
<li>최근 등장하는 표준 스펙 또는 오픈소스 프레임워크에서는 API가 발생시키는 예외를 언체크 에외로 정의하는 것이 일반화되고 있다.</li>
<li>대개 복구 불가능한 상황이고 RuntimeException 등으로 포장해서 던져야 할 테니 아예 API 차원에서 런타임 예외를 던지도록 만들어서 그 밖의 메소드들이 신경 쓰지 않게 해주는 편이 낫다는 것이다.</li>
</ul>
<pre><code class="language-java">public class DuplicateUserIdException extends RuntimeException {
    public DuplicateUserIdException(Throwable cause) {
        super(cause);
    }
}

public void add() thorws DuplicateUserIdException {
    try {
        //
    } catch (SQLException e) {
        if (e.getErrorCode() == MysqlErrorNubmers.ER_DUP_ENTRY)
            throw new DuplicateUserIdException(e); //예외 전환
        else
            throw new RuntimeException(e); //예외 포장
    }
}</code></pre>
<p>→ 낙관적인 예외처리 기법. 복구할 수 있는 예외는 없다고 가정하고 예외가 생겨도 런타임 예외이므로 시스템 레벨에서 처리하거나 꼭 필요한 경우는 런타임 예외를 잡아 복구하거나 대응해줄 수 있다는 태도를 기반으로 함. </p>
<Br>

<h3 id="애플리케이션-예외">애플리케이션 예외</h3>
<ul>
<li><p>외부의 예외상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 예외를 발생시키고, 반드시 catch해서 조치를 취하도록 요구하는 예외</p>
</li>
<li><p>ex) 잔고 부족 상황에서 출금처리를 하려고 할 때 → 비즈니스적인 의미를 띤 예외를 던지도록 하고, 체크 예외로 만들어 에외상황에 대한 로직을 구현하도록 강제한다.</p>
<pre><code class="language-java">  try {
      BigDecimal balance = account.withdraw(amount);
  } catch(InsufficientBalanceException e) {
      BigDecimal availFunds = e.getAvailFunds();
      //잔고 부족 메시지를 준비하고 이를 출력하도록 진행
  }</code></pre>
</li>
</ul>
<br>

<h2 id="422-db-에러-코드-매핑을-통한-전환">4.2.2 DB 에러 코드 매핑을 통한 전환</h2>
<h3 id="db-종류가-바뀌더라도-dao를-수정하지-않으려면">DB 종류가 바뀌더라도 DAO를 수정하지 않으려면</h3>
<p>아래의 두 가지 문제를 해결해야 한다. </p>
<ol>
<li><p>SQLException의 비표준 에러 코드</p>
<pre><code class="language-java"> if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) { ... </code></pre>
<p> → DB별로 DB에러 코드가 다르기 때문에 DB 독립적인 코드가 될 수 없다. </p>
</li>
<li><p>SQLException이 제공하는 SQL 상태 정보</p>
<ul>
<li><p>Open Group의 XOPEN SQL 스펙에 정의된 상태코드</p>
<ul>
<li>통신장애로 DB연결에 실패한 경우 08S01</li>
<li>테이블이 존재하지 않는 경우 42S02</li>
</ul>
<p>→ DB의 JDBC드라이버에서 SQLException을 담을 상태 코드를 정확하게 만들어주지 않는 문제가 있다. </p>
</li>
</ul>
</li>
</ol>
<br>

<h3 id="해결책은">해결책은,</h3>
<ul>
<li>DB 업체별로 만들어 유지하고 있는 DB 전용 에러 코드를 사용해 예외의 원인이 무엇인지 해석해주는 기능을 만든다.<ul>
<li>ex) 중복 오류가 발생하는 경우 MySQL-1062, Oracle-1 등의 에러 코드를 받게 되는데, 이 코드를 DuplicateKeyException이라는 의미가 분명히 드러나는 예외로 전환해준다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[토비의 스프링 3.1] 3주차 스터디 - 탬플릿]]></title>
            <link>https://velog.io/@yul_ee/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-3.1-3%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-%ED%83%AC%ED%94%8C%EB%A6%BF</link>
            <guid>https://velog.io/@yul_ee/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-3.1-3%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-%ED%83%AC%ED%94%8C%EB%A6%BF</guid>
            <pubDate>Fri, 24 Jan 2025 06:38:18 GMT</pubDate>
            <description><![CDATA[<p><code>스터디 날짜</code> 24.07.23.
<code>스터디 범위</code> 3장. 탬플릿</p>
<hr>
<h2 id="322">3.2.2</h2>
<h3 id="탬플릿-메서드-패턴">탬플릿 메서드 패턴</h3>
<img width="688" alt="Untitled" src="https://github.com/user-attachments/assets/3498f1ac-a704-4fa0-b1c6-053a013ff7e0" />

<ul>
<li><p>상속을 통해 기능을 확장한다.
  → 상위 클래스에 불필요한 변화가 생기지 않도록 할 수 있으니 객체지향 설계의 핵심 원리인 OCP를 그럭저럭 지킬 수 있다. </p>
</li>
<li><p>매번 상속을 통해 서브클래스를 만들어야 한다.</p>
</li>
<li><p>확장구조가 클래스 설계 시점에서 고정되어 버린다.
  → 관계에 대한 유연성이 떨어진다. </p>
</li>
</ul>
<br>

<h3 id="전략-패턴">전략 패턴</h3>
<img width="704" alt="Untitled (1)" src="https://github.com/user-attachments/assets/26cc44ee-8408-469f-b274-889167780b64" />


<ul>
<li><p>클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 패턴이다.</p>
</li>
<li><p>Context에서 일정한 구조를 가지고 동작하고, 특정 확장 기능은 Strategy 인터페이스를 통해 외부의 독립된 전략 클래스에 위임한다.
  → 복잡한 알고리즘을 <strong>바꿔가면서</strong> 사용할 수 있다는 것이 핵심</p>
</li>
<li><p>Context가 구체적인 전략 클래스를 직접 알고 있다면 OCP에 잘 들어맞는다고 볼 수 없다.
  → Context를 사용하는 앞단의 Client가 어떤 전략을 사용할지 직접 정해서 Context에 전달하는 방식으로 해결할 수 있다. </p>
</li>
</ul>
<img width="704" alt="Untitled (1)" src="https://github.com/user-attachments/assets/1826c3c0-d24f-45d9-9a91-df6f946848cd" />


<p>→ 전략 오브젝트 생성과 컨텍스트로의 전달을 담당하는 책임을 분리하는 과정을 일반화시킨 것이 <code>DI</code>. 즉, <code>DI</code>란 전략패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조인 것.</p>
<br>


<h2 id="332">3.3.2</h2>
<h3 id="로컬-클래스">로컬 클래스</h3>
<ul>
<li>클래스 파일이 많아지는 문제를 해결할 수 있다.</li>
</ul>
<blockquote>
<p>💡 중첩 클래스의 종류</p>
</blockquote>
<ul>
<li><p><code>Static Class</code>: 독립적으로 오브젝트로 만들어 질 수 있음</p>
</li>
<li><p><code>Inner Class</code>: 자신이 정의된 클래스의 오브젝트 안에서만 만들어질 수 있음</p>
<ul>
<li><code>멤버 내부 클래스</code>: 멤버 필드처럼 오브젝트 레벨에 정의됨</li>
<li><code>로컬 클래스</code>: 메서드 레벨에 정의됨. 로컬 변수를 선언하듯이 선언하면 됨</li>
<li><code>익명 내부 클래스</code>: 이름을 갖지 않으며 선언된 위치에 따라서 scope를 다르게 가짐</li>
</ul>
</li>
<li><p>특정 메서드에서만 사용되는 클래스라면 로컬 클래스를 바로 정의해서 쓰는 것도 나쁘지 않음</p>
</li>
<li><p>내부 메서드는 자신이 정의된 메서드의 로컬 변수에 직접 접근할 수 있다는 장점도 있음. 다만, 외부 변수를 사용할 때는 외부 변수를 반드시 final로 선언할 것!</p>
</li>
</ul>
<br>


<h3 id="익명-내부-클래스">익명 내부 클래스</h3>
<ul>
<li><p>클래스 선언과 오브젝트 생성이 결합된 형태</p>
</li>
<li><p>클래스를 재사용할 필요가 없고, 구현한 인터페이스 타입으로만 사용할 때 유용함</p>
<ul>
<li><p>구현한 인터페이스를 생성자처럼 이용해서 오브젝트로 만든다.</p>
<pre><code class="language-java">  //StatementStrategy -&gt; 인터페이스
  StatementStrategy st = new StatementStrategy() {

      ...

  };</code></pre>
</li>
</ul>
</li>
</ul>
<br>


<h2 id="342">3.4.2</h2>
<h3 id="인터페이스를-사용하지-않고-di를-적용하는-것은-문제가-있지-않을까">인터페이스를 사용하지 않고 DI를 적용하는 것은 문제가 있지 않을까?</h3>
<ul>
<li>스프링의 DI는 객체의 생성과 관계 설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포괄한다.</li>
<li>그런 의미에서 인터페이스가 아닌 클래스를 주입했더라도 DI의 기본을 따르고 있다고 이야기할 수 있다.</li>
</ul>
<br>


<h3 id="어떤-경우에-인터페이스가-아닌-클래스를-di-구조로-괜찮은걸까">어떤 경우에 인터페이스가 아닌 클래스를 DI 구조로 괜찮은걸까?</h3>
<ol>
<li>싱글톤으로 등록되어 여러 오브젝트에서 공유해 사용되는 것이 이상적일 때</li>
<li>클래스가 DI를 통해 다른 빈에 의존하고 있는 경우, 오브젝트를 주입받기 위해서라도 마찬가지로 빈으로 등록되어야 한다.</li>
</ol>
<p>→ 다만, 클래스를 바로 사용하는 코드 구성은 가장 마지막 단계에서 고려해야 한다..</p>
<br>


<h2 id="35-템플릿콜백-패턴">3.5 템플릿/콜백 패턴</h2>
<ul>
<li><code>템플릿</code>: 전략 패턴의 컨텍스트를 의미한다.
  → 고정되는 틀의 로직</li>
<li><code>콜백</code>: 익명 내부 클래스로 만들어지는 오브젝트를 의미한다.
  → 템플릿 안에서 호출되는 것을 목적으로 한다.</li>
</ul>
<br>

<h3 id="35-1-탬플릿콜백의-동작원리">3.5. 1 탬플릿/콜백의 동작원리</h3>
<p><strong>탬플릿/콜백의 특징</strong></p>
<ul>
<li>탬플릿/콜백 패턴의 <code>콜백</code>은 보통 단일 메소드 인터페이스를 사용한다.<ul>
<li>탬플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문</li>
<li>ex) DB connection</li>
<li>즉, 콜백은 일반적으로 하나의 메서드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다고 보면 된다.</li>
</ul>
</li>
</ul>
<br>    

<p><strong>탬블릭/콜백의 작업 흐름</strong>
<img width="616" alt="Untitled (3)" src="https://github.com/user-attachments/assets/b9905f9f-44cf-43eb-a4c2-6f4311aed51e" /></p>
<ul>
<li><code>클라이언트</code>의 역할은 탬플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공하는 것이다. 만들어진 콜백은 클라이언트가 탬플릿의 메소드를 호출할 때 <strong>파라미터</strong>로 전달된다.</li>
<li><code>탬플릿</code>은 정해진 작업 흐름대로 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드를 호출한다. </li>
<li><code>콜백</code>은 클라이언트 메소드에 있는 정보 + 탬플릿이 제공한 참조정보를 이용해서 작업을 수행하고, 그 결과를 다시 탬플릿에 돌려준다.</li>
<li><code>탬플릿</code>은 콜백이 돌려준 정보를 사용해 작업을 마저 진행한다. 경우에 따라서는 최종 결과를 다시 클라이언트에 돌려주기도 한다.</li>
<li><strong>이러한 작업 흐름의 특징</strong><ul>
<li>매번 메서드 단위로 사용할 오브젝트를 새롭게 전달받는다.</li>
<li>콜백 오브젝트가 내부 클래스로서 자신을 생성한 클라이언트 메서드 내의 정보를 직접 참조한다.</li>
<li>클라이언트와 콜백이 강하게 결합된다.</li>
</ul>
</li>
</ul>
<p>-&gt; 탬플릿/콜백은 전략패턴 + DI의 장점을 익명 내부 클래스 사용 전략과 결합한 독특한 활용법이다. 이를 하나의 고유한 디자인 패턴으로 기억해두면 편리하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[토비의 스프링 3.1] 2주차 스터디 - 테스트]]></title>
            <link>https://velog.io/@yul_ee/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-3.1-2%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@yul_ee/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-3.1-2%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Fri, 24 Jan 2025 06:31:18 GMT</pubDate>
            <description><![CDATA[<p><code>스터디 날짜</code> 24.07.15.
<code>스터디 범위</code> 2장. 테스트</p>
<hr>
<h2 id="2장-테스트">2장 테스트</h2>
<blockquote>
<p>💬 스프링이 개발자에게 제공하는 가장 중요한 가치가 무엇이냐고 질문한다면 나는 주저하지 않고 객체지향과 테스트라고 대답할 것이다.</p>
</blockquote>
<h2 id="21-userdaotest-다시-보기">2.1 UserDaoTest 다시 보기</h2>
<h3 id="작은-단위의-테스트-unit-test">작은 단위의 테스트 (Unit Test)</h3>
<ul>
<li><p>테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 한다. 관심사의 분리가 여기에도 적용된다.</p>
<ul>
<li>사용할 DB의 상태를 테스트가 관장하고 있다면 → 단위 테스트 O</li>
<li>DB의 상태가 매번 달라지고, 테스트를 위해 DB를 특정 상태로 만들어줄 수 없다면 → 단위 테스트 X</li>
</ul>
</li>
<li><p>단위 테스트가 필요한 이유</p>
<ul>
<li>개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위함</li>
</ul>
</li>
</ul>
<br>

<h2 id="22-userdaotest-개선">2.2 UserDaoTest 개선</h2>
<blockquote>
<p>💬 켄트 벡, “테스트란 개발자가 마음 편하게 잠자리에 들 수 있게 해주는 것”</p>
</blockquote>
<h3 id="테스트-메소드-전환">테스트 메소드 전환</h3>
<ul>
<li><p>테스트 메소드는 JUnit 프레임워크가 요구하는 조건 두 가지를 따라야 한다.</p>
<ol>
<li><p>메소드가 public으로 선언되어야 한다.</p>
</li>
<li><p>메소드에 @Test라는 어노테이션을 붙인다. </p>
<p>(+리턴값이 void형이고 파라미터가 없어야 한다.)</p>
</li>
</ol>
</li>
</ul>
<br>

<h3 id="검증-코드-전환">검증 코드 전환</h3>
<ul>
<li><p>기존의 테스트 코드</p>
<pre><code class="language-java">  if (!user.getName().equals(user2.getName())) { ... }</code></pre>
</li>
<li><p>JUnit의 스태틱 메소드 assertThat을 이용한 테스트 코드</p>
<pre><code class="language-java">  assertThat(user2.getName(), is(user.getName()));</code></pre>
<ul>
<li>is()는 <code>matcher</code>의 일종으로, equals()로 비교해주는 기능을 가진다. 
첫 번째 파라미터를 매처와 비교해서 일치하면 다음으로 넘어가고, 아니면 테스트가 실패한다.</li>
</ul>
</li>
</ul>
<Br>

<h2 id="23-개발자를-위한-테스팅-프레임워크-junit">2.3 개발자를 위한 테스팅 프레임워크 JUnit</h2>
<p>JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다.</p>
<p>모든 테스트는 실행 순서에 상관없이 독립적으로 항상 동일한 결과를 낼 수 있도록 해야 한다. </p>
<h3 id="예외조건에-대한-테스트">예외조건에 대한 테스트</h3>
<ul>
<li><p>예외 던지기</p>
<ul>
<li>테스트 중에 예외가 던져지면 테스트 메소드의 실행은 중단되고 테스트는 실패한다.</li>
<li>예외 발생 여부는 메소드를 실행해서 리턴 값을 비교하는 방법으로 확인할 수 없다. 
즉, <code>assertThat()</code>메소드로는 검증이 불가능하다.</li>
</ul>
</li>
<li><p>JUnit의 예외 테스트 기능을 이용하면 예외조건을 보다 쉽게 검증할 수 있다.</p>
<pre><code class="language-java">  // 테스트 중에 발생할 것으로 기대하는 예외 클래스를 지정해준다. 
  @Test(expected=EmptyResultDataAccessException.class)</code></pre>
</li>
</ul>
<br>

<h3 id="포괄적인-테스트">포괄적인 테스트</h3>
<blockquote>
<p>💬 로드 존슨, “항상 네거티브 테스트를 먼저 만들라”</p>
</blockquote>
<ul>
<li>테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다.</li>
<li>예외 케이스를 확인할 수 있는 테스트를 먼저 만들려고 한다면 이 예외적인 상황을 빠뜨리지 않는 꼼꼼한 개발을 할 수 있게 된다.</li>
</ul>
<br>

<h3 id="기능설계를-위한-테스트">기능설계를 위한 테스트</h3>
<ul>
<li>테스트 코드는 마치 잘 작성된 하나의 기능정의서처럼 보인다.<ul>
<li>조건: 어떤 조건을 가지고</li>
<li>행위: 무엇을 할 때</li>
<li>결과: 어떤 결과가 나온다</li>
</ul>
</li>
</ul>
<br>

<h3 id="테스트-주도-개발-test-driven-development">테스트 주도 개발, Test Driven Development</h3>
<blockquote>
<p>💬 만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고, 테스트를 <strong>성공</strong>하게 해주는 코드를 작성하는 방식의 개발</p>
</blockquote>
<ul>
<li>“실패한 테스트를 성공시키기 위한 목적이 아닌 코드를 만들지 않는 것”이 기본원칙</li>
<li>머릿속에서 진행되는 테스트는 제약이 심하고, 오류가 많고, 나중에 다시 반복하기가 힘들다. 
차라리 머릿속에서 복잡하게 진행하던 작업을 실제 코드로 끄집어 놓으면 그게 바로 TDD가 된다.</li>
</ul>
<p>→ (정원용 멘토님) 개발 초반에 TDD를 하게 되면 코드를 변경하거나 리팩터링 해야할때 테스트코드가 망가진다는 생각에 코드 수정을 망설이게 될 수 있다…</p>
<Br>

<h3 id="junit이-테스트를-수행하는-방식">JUnit이 테스트를 수행하는 방식</h3>
<ol>
<li><p>테스트 클래스에서 <code>@Test</code>가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다. </p>
</li>
<li><p><strong><em>테스트 클래스의 오브젝트를 하나 만든다.</em></strong> </p>
<ul>
<li><p>왜 매번 새로운 오브젝트를 만들까?</p>
<blockquote>
<p>💁 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해서 매번 새로운 오브젝트를 만든다.</p>
</blockquote>
</li>
</ul>
</li>
<li><p><code>@Before</code>가 붙은 메소드가 있으면 실행한다. </p>
</li>
<li><p><code>@Test</code>가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다. </p>
</li>
<li><p><code>@After</code>가 붙은 메소드가 있으면 실행한다. </p>
</li>
<li><p>나머지 테스트 메소드에 대해 2~5번을 반복한다. </p>
</li>
<li><p>모든 테스트의 결과를 종합해서 돌려준다. </p>
</li>
</ol>
<br>

<h3 id="픽스처">픽스처</h3>
<blockquote>
<p>💬 테스트를 수행하는 데 필요한 정보나 오브젝트를 말한다. 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용되므로 <code>@Before</code> 메소드를 이용해 생성해두면 편리하다.</p>
</blockquote>
<br>

<h2 id="24-스프링-테스트-적용">2.4 스프링 테스트 적용</h2>
<h3 id="beforeclass-스태틱-메소드">@BeforeClass 스태틱 메소드</h3>
<p>테스트 클래스 전체에 걸쳐 딱 한번만 실행된다. </p>
<p>여러 테스트가 함께 참조할 <strong>애플리케이션 컨텍스트를 오브젝트 레벨에 저장해두면 곤란하므로</strong>, 스태틱 필드에 애플리케이션 컨텍스트를 저장해두는 방식을 고민해볼 수 있다. </p>
<ul>
<li><em>애플리케이션 컨텍스트를 오브젝트 레벨이 저장하게 되면 생기는 문제들</em><ul>
<li>빈이 많아지면 애플리케이션 컨텍스트 생성에 적지 않은 시간이 소요된다.</li>
<li>애플리케이션 컨텍스트가 만들어질 때 모든 싱글톤 빈 오브젝트를 초기화하는데 이때도 상당한 시간을 필요로 한다.</li>
<li>어떤 빈은 많은 리소스를 필요로하거나 독립적인 스레드를 띄우기도 하는데, 테스트를 마칠 때 애플리케이션 컨텍스트 내의 빈이 할당한 리소스를 release해주지 않으면 다음 테스트에 문제가 생길 수도 있다.</li>
</ul>
</li>
</ul>
<p>하지만 스프링이 직접 제공하는 애플리케이션 컨텍스트 테스트 지원 기능을 사용하는게 더 편리하다!</p>
<Br>

<h3 id="스프링-테스트-컨텍스트-프레임워크-적용">스프링 테스트 컨텍스트 프레임워크 적용</h3>
<ul>
<li><p>ApplicationContext 타입의 인스턴스 변수를 선언하고 <code>@AutoWired</code> 어노테이션을 붙인다.</p>
</li>
<li><p>클래스 레벨에 <code>@RunWith</code> 어노테이션을 붙여 JUnit 확장기능을 지정한다.</p>
</li>
<li><p>클래스 레벨에 <code>@ContextConfiguration</code> 어노테이션을 붙여 테스트 컨텍스트가 자동으로 만들어줄 애플리케이션 컨텍스트의 위치를 지정한다.</p>
<p>  → 같은 설정파일을 가진 애플리케이션 컨텍스트를 사용하면, 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유할 수 있다. (성능 대폭 향상)</p>
</li>
</ul>
<br>

<h3 id="autowired">@AutoWired</h3>
<ul>
<li>타입에 의한 자동 와이어링<ul>
<li>AutoWired가 붙은 <code>변수 타입</code>과 일치하는 컨텍스트 내의 빈을 찾고, 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다. 이때 생성자나 수정자 메소드가 없어도 주입이 가능하다.</li>
<li>단, 같은 타입의 빈이 두 개 이상 있는 경우에는 타입만으로 어떤 빈을 가져올지 결정할 수 없기 때문에 <code>변수 이름</code>과 같은 이름의 빈이 주입된다.</li>
<li>변수 이름으로도 빈을 찾을 수 없다면 <code>예외</code>가 발생한다.</li>
</ul>
</li>
</ul>
<Br>

<h3 id="di와-테스트">DI와 테스트</h3>
<blockquote>
<p>❓ 구현 클래스를 절대 바꾸지 않을 경우에도 굳이 인터페이스를 사용하고 DI를 통해 주입해주는 방식을 이용해야 하는가? → 🙆‍♀️</p>
</blockquote>
<ol>
<li>소프트웨어 개발에서 절대로 바뀌지 않는 것은 없다. </li>
<li>인터페이스를 두고 DI를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있다. </li>
<li>DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는 데 중요한 역할을 한다. </li>
</ol>
<br>

<h3 id="테스트-코드에-의한-di">테스트 코드에 의한 DI</h3>
<ul>
<li><p>테스트에서 운영용 DB를 사용하는 것은 위험할 수 있다.</p>
</li>
<li><p>테스트 코드에 의한 DI를 이용해서 테스트 중에 DAO가 사용할 DataSource 오브젝트를 바꿔주면 된다.</p>
<pre><code class="language-java">  @DirtiesContext  -&gt; 테스트 메소드에서 애플리케이션 컨텍스트의 구성이나 상태를 변경
  public class UserDatTest {
      @Autowired
      UserDao dao;

      @Before
      public void setUp() {
          DataSource dataSource = new SingleConnectionDataSource(
              &quot;jdbc:mysql://localhost/testdb&quot;, &quot;spring&quot;, &quot;book&quot;, true);
              dao.setDataSource(dataSource); // -&gt; 코드에 의한 수동 DI
      }</code></pre>
<ul>
<li><p>장점</p>
<ul>
<li>설정파일을 수정하지 않고도 테스트 코드를 통해 오브젝트 관계를 재구성할 수 있음</li>
<li>예외상황을 만들고자 일부러 엉뚱한 오브젝트를 넣거나, 테스트용오르 준비된 오브젝트를 사용하게끔 할 수 있음</li>
</ul>
</li>
<li><p>@DirtiesContext</p>
<ul>
<li><p>이 어노테이션이 붙은 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않으므로, 테스트 중에 변경한 컨텍스트가 뒤의 테스트에 영향을 주지 않는다.</p>
</li>
<li><p>이 어노테이션이 붙어있는 클래스나 메서드에서 애플리케이션 컨텍스트를 새로 만들고, 실행이 끝나고 나면 해당 컨텍스트는 폐기하고 다시 새로운 애플리케이션 컨텍스트가 만들어진다.</p>
<p>→ 아무래도 찜찜하므로, 그냥 테스트용 설정 파일을 따로 만드는게 낫다! 번거롭게 수동 DI 하거나 위의 어노테이션을 달아줄 필요가 없어짐 👍</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<h3 id="컨테이너-없는-di-테스트">컨테이너 없는 DI 테스트</h3>
<ul>
<li>애플리케이션 컨텍스트를 사용하지 않고 직접 오브젝트를 만들고 수동 DI 해준다.</li>
<li>이 방법은 테스트 수행 속도가 가장 빠르고, 테스트가 간결해지므로 테스트 방법을 선택할 때 컨테이너 없이 테스트할 수 있는 방법을 최우선적으로 고려한다.</li>
</ul>
<br>

<h2 id="25-학습-테스트로-배우는-스프링">2.5 학습 테스트로 배우는 스프링</h2>
<blockquote>
<p>💬 자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리에 대해 작성하는 테스트를 학습테스트라고 한다. 테스트이지만 기능 검증이 목적이 아니며, 기술이나 기능을 자신이 얼마나 제대로 <strong>이해</strong>하고 있는지를 검증하는 것이 목적이다.</p>
</blockquote>
<h3 id="학습-테스트의-장점">학습 테스트의 장점</h3>
<ul>
<li>다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.</li>
<li>학습 테스트 코드를 개발 중에 참고할 수 있다.</li>
<li>프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.</li>
</ul>
<p>스프링 배포판의 압축을 풀어보면 프레임워크 소스코드와 함께 테스트 코드를 살펴볼 수 있다 → 좋은 공부자료</p>
<Br>

<h3 id="테스트-검증-방법">테스트 검증 방법</h3>
<ol>
<li>assertThat(param, matcher)</li>
<li>assertTrue(param)</li>
<li>assertThat(param, <code>either</code>(is(nullValue())).<code>or</code>(is(this.context))));</li>
</ol>
<br>

<h3 id="버그-테스트">버그 테스트</h3>
<blockquote>
<p>💬 버그 테스트란 코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트를 말한다. 버그 테스트는 일단 실패하도록 만들고, 이후에 버그 테스트가 성공할 수 있도록 애플리케이션 코드를 수정한다.</p>
</blockquote>
<ul>
<li>테스트의 완성도를 높여준다.</li>
<li>버그의 내용을 명확하게 분석하게 해준다.</li>
<li>기술적인 문제를 해결하는 데 도움이 된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[토비의 스프링 3.1] 1주차 스터디 - 오브젝트와 의존관계]]></title>
            <link>https://velog.io/@yul_ee/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-3.1-1%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8%EC%99%80-%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@yul_ee/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-3.1-1%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8%EC%99%80-%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84</guid>
            <pubDate>Fri, 24 Jan 2025 06:08:31 GMT</pubDate>
            <description><![CDATA[<p><code>스터디 날짜</code> 24.07.05.
<code>스터디 범위</code> 1장. 오브젝트와 의존관계</p>
<blockquote>
<p>스프링이 가장 관심을 많이 두는 대상은 오브젝트다. 스프링을 이해하려면 먼저 오브젝트에 깊은 관심을 가져야 한다. 애플리케이션에서 오브젝트가 생성되고 다른 오브젝트와 관계를 맺고, 사용되고, 소멸하기까지의 전 과정을 진지하게 생각해볼 필요가 있다.</p>
</blockquote>
<br>

<hr>
<h2 id="pojo-프로그래밍">POJO 프로그래밍</h2>
<blockquote>
<p>💬 스프링은 자바의 기술이 복잡해지면서 잃어버린 객체지향 언어의 본질과 장점을 되살릴 수 있도록 도와주는 도구이다. 스프링은 가장 단순한 객체지향적인 개발 모델, <strong>POJO 프로그래밍</strong>을 주장한다.</p>
</blockquote>
<p>진정한 <strong>POJO란,</strong>
<img width="252" alt="1 png" src="https://github.com/user-attachments/assets/59f1bfe5-3770-4437-9113-29a095d3f62b" /></p>
<ul>
<li>객체지향적인 원리에 충실하면서</li>
<li>특정 환경과 기술에 종속되지 않고</li>
<li>필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트를 말한다.</li>
</ul>
<br>

<p><strong>공통 프로그래밍 모델</strong></p>
<ol>
<li><p>IoC/DI
 오브젝트의 생명주기와 의존관계에 대한 프로그래밍 모델이다.
 유연하고 확장성이 뛰어난 코드를 만들 수 있게 도와주는 객체지향 설계 원칙과 디자인 패턴의 핵심 원리를 담고 있으며 스프링 프레임워크의 근간이 된다. </p>
</li>
<li><p>PSA (서비스 추상화)</p>
<p> 구체적인 기술과 환경에 종속되지 않도록 유연한 추상 계층을 두어 이식성이 뛰어나며 유연한 애플리케이션을 만들 수 있게 된다. </p>
</li>
<li><p>AOP</p>
<p> 애플리케이션 코드에서 부가적인 기능을 독립적으로 모듈화하는 프로그래밍 모델이다. </p>
</li>
</ol>
<br>

<h2 id="11-초난감-dao">1.1 초난감 DAO</h2>
<blockquote>
<p>💁 DAO(Data Access Object)는 DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트를  말한다.</p>
</blockquote>
<blockquote>
<p>💬 사용자 정보를 저장할 때는 <strong>자바빈 규약</strong>을 따르는 오브젝트를 이용하면 편리하다.</p>
</blockquote>
<Br>

<p>✅ <strong>자바빈 규약의 규칙</strong></p>
<ol>
<li><p>자바빈은 기본 패키지 이외의 특정 패키지에 속해 있어야 한다. </p>
</li>
<li><p>기본 생성자가 존재해야 한다. </p>
</li>
<li><p>멤버변수의 접근제어자는 private로 선언되어야 한다. </p>
</li>
<li><p>멤버변수에 접근 가능한 getter와 setter 메서드가 존재해야 한다. (3번의 규칙을 지키기 위함)</p>
</li>
<li><p>getter와 setter는 접근제한자가 public으로 선언되어야 한다. </p>
</li>
<li><p><strong>직렬화</strong> 되어 있어야 한다. (선택사항)</p>
<ul>
<li><p>직렬화란?
  <a href="https://yjksw.github.io/java-bean/">참고 링크</a></p>
<ul>
<li><p><strong>Serialize,</strong>
직렬화를 위한 인터페이스로 객체를 파일에 저장하거나, 다른 서버로 보내거나 받거나 등의 일을 하기 위해 구현한다.</p>
</li>
<li><p><strong>자바 객체를 serialize하기 위해서는,</strong>
<code>java.io.Serializable</code> 인터페이스를 구현하도록 한다. 해당 인터페이스는 멤버변수나 메서드가 존재하지 않는 maker interface이다.</p>
</li>
<li><p><strong>Serialization 특징</strong></p>
</li>
</ul>
<ol>
<li><p>부모 클래스가 <code>Serializable</code> 인터페이스를 구현하면 자식 클래스는 자동으로 <code>Serializable</code>하다.</p>
</li>
<li><p>non-static 변수만 Serialization으로 저장될 수 있다.</p>
</li>
<li><p>보안 등의 이유로 어떤 멤버 변수가 Serialization되지 않기를 원한다면 해당 데이터를 <code>transient</code> 데이터로 지정하도록 한다.</p>
<pre><code class="language-java">class Member implements Serializable {
   transient String password;
   String name;
   ...
}</code></pre>
</li>
<li><p>해당 객체가 deserialized 될 때 해당 객체의 생성자는 호출되지 않는다.</p>
</li>
<li><p><code>Serializable</code>한 객체와 연관되어 있는 객체 또한 <code>Serializable</code> 인터페이스를 반드시 구현해야 한다.</p>
<pre><code class="language-java">class ObjectA implements Serializable {  
   //ObjectB는 반드시 Serializable을 구현해야 함  
   ObjectB oj = new ObjectB();
}</code></pre>
<blockquote>
<p><code>Serializable</code>을 implement하여 직렬화가 가능하다. 해당 객체에 영속성을 부여하기 위해서 사용되는 매커니즘이다. </p>
</blockquote>
</li>
</ol>
</li>
<li><p>marker interface란?
<a href="https://kjhoon0330.tistory.com/entry/Java-%EB%A7%88%EC%BB%A4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC">참고 링크</a></p>
<blockquote>
<p>💁 인터페이스 내부에 아무 것도 없는 인터페이스로, 객체의 타입과 관련된 정보를 제공해 준다. 컴파일러와 JVM은 이 마커 인터페이스를 통해 객체에 대한 추가적인 정보를 얻을 수 있다.</p>
</blockquote>
<ul>
<li><p><strong>❓객체의 타입과 관련된 정보를 제공해준다</strong>
<strong>NotSerializableException</strong></p>
<pre><code class="language-java">import java.io.*;

public class MarkerInterfaceTest {
   public void serializableTest() throws IOException {
       File f = new File(&quot;test.txt&quot;);
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(f));

       // 이 부분이 객체를 파일로 저장하는 부분
       objectOutputStream.writeObject(new SomeObject(&quot;kjhoon&quot;, &quot;kjhoon0330@snu.ac.kr&quot;));
   }

   public static void main(String[] args) {
       MarkerInterfaceTest t = new MarkerInterfaceTest();
       try {
           t.serializableTest();
       } catch (Exception e) {
           e.printStackTrace();
       }
   }
}

// 인터페이스를 구현하지 않은 임의의 오브젝트
class SomeObject {
   private String name;
   private String email;

   public SomeObject(String name, String email) {
       this.name = name;
       this.email = email;
   }
}</code></pre>
<ul>
<li><strong>해결 방법</strong><pre><code class="language-java">class SomeObject implements Serializable {
// Serializlble 인터페이스 구현
// 내부 생략
}</code></pre>
</li>
<li><strong>해결된 이유</strong>
아래 사진은 <code>ObjectOutputStream</code>의 <code>writeObject</code> 함수 내부를 보여준다. <code>obj</code>가 <code>Serializable</code>의 인스턴스인 경우 <code>NotSerializableException</code>이 발생되지 않도록 처리하고 있다. 이는 <code>Serializable</code>이라는 마커 인터페이스가 객체에 추가적인 정보를 주입시키고 있음을 이야기한다.</li>
</ul>
<img width="770" alt="2 png" src="https://github.com/user-attachments/assets/093a3803-50a9-4634-ac8e-702f9b795c18" />

</li>
</ul>
</li>
</ul>
</li>
</ol>
<Br>

<h2 id="12-dao의-분리">1.2 DAO의 분리</h2>
<blockquote>
<p>개발자가 객체를 설계할 때 가장 염두에 둬야 할 사항은 바로 미래의 변화를 어떻게 대비할 것인가이다. 객체지향 설계와 프로그래밍이 이전의 절차적 프로그래밍 패러다임에 비해 초기에 좀 더 많은, 번거로운 작업을 요구하는 이유는 객체지향 기술 자체가 지니는, 변화에 효과적으로 대처할 수 있다는 기술적인 특징 때문이다.</p>
</blockquote>
<Br>


<p><strong>💁 템플릿 메소드 패턴</strong></p>
<ul>
<li>슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든다.</li>
<li>서브클래스에서는 추상 메소드를 구현하거나, <em>훅 메소드</em>를 오버라이드하는 방법을 이용해 기능의 일부를 확장한다.<ul>
<li>슈퍼클래스에서 디폴트 기능을 정의해두거나 비워뒀다가 서브클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메소드를 말한다.</li>
</ul>
</li>
</ul>
<p><strong>💁 팩토리 메소드 패턴</strong></p>
<ul>
<li>서브클래스에서 구체적인 오브젝트 생성 방법을 결정하게 하는 방법</li>
<li>주로 인터페이스 타입으로 오브젝트를 리턴하므로 서브클래스에서 정확히 어떤 클래스의 오브젝트를 만들어 리턴할지는 슈퍼클래스에서는 알지 못하며 관심도 없다.</li>
</ul>
<p><img src="https://github.com/user-attachments/assets/e61bc02d-e04f-4256-a935-7d8f73673cc5" alt="3 png"></p>
<p><strong>💆‍ 관심사의 분리가 가능해졌지만, 상속을 사용하는 단점이 존재한다.</strong></p>
<ul>
<li>상속을 통한 상하위 클래스의 관계는 생각보다 밀접하다.</li>
<li>서브클래스는 슈퍼클래스의 기능을 직접 사용할 수 있다. 즉, 슈퍼클래스 내부의 변경이 있을 때 모든 서브클래스를 함께 수정하거나 다시 개발해야 할 수도 있다.</li>
</ul>
<Br>

<p>*디자인 패턴을 공부할 땐, 패턴을 적용할 상황, 해결해야 할 문제, 솔루션의 구조와 각 요소의 역할과 함께 핵심 의도가 무엇인지를 기억해둬야 한다.</p>
<Br>
<Br>

<h2 id="13-dao의-확장">1.3 DAO의 확장</h2>
<h3 id="131-클래스의-분리">1.3.1 클래스의 분리</h3>
<p>  상속을 사용하는 방식 대신, 관심사가 다른 두 부분을 클래스로 분리한다.
  다만 이 경우 아래의 문제가 재차 발생한다.</p>
<p>  -&gt; 하나의 클래스가 다른 클래스와 그 코드에 종속적이면 자유로운 확장이 힘들다. 어떤 클래스에서 어떤 메서드를 가져와야하는지 일일이 지정하지 않아도 괜찮을 순 없을까?</p>
<h3 id="133-관계설정-책임의-분리">1.3.3 관계설정 책임의 분리</h3>
<ul>
<li>클래스 사이에 존재하는 불필요한 의존관계를 끊어내자.</li>
<li>어떤 클래스를 사용할 것인지는 <code>클라이언트</code>의 책임으로 전가해서 런타임 시점에 다이내믹한 오브젝트 관계를 가지게끔 하는 것이다.
<img src="https://velog.velcdn.com/images/yul_ee/post/7f7b132e-fe1b-4f28-a2c6-24f432eef313/image.png" alt=""></li>
</ul>
<br>

<h3 id="134-원칙과-패턴">1.3.4 원칙과 패턴</h3>
<p><strong>개방 폐쇄 원칙</strong></p>
<ul>
<li>클래스나 모듈은 얼마든지 기능을 확장할 수 있다. (확장에는 열려있고,)</li>
<li>동시에 자신의 핵심 기능을 구현한 코드는 그런 변화에 영향을 받지 않고 유지할 수 있다. (변경에는 닫혀있음을 의미)</li>
</ul>
<p><strong>높은 응집도와 낮은 결합도</strong></p>
<ul>
<li>응집도가 높다는 건 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 뜻이다.</li>
<li>책임과 관심사가 다른 오브젝트 또는 모듈과는 낮은 결합도, 즉 느슨하게 연결된 형태를 유지하는 것이 바람직하다.</li>
</ul>
<br>

<h2 id="14-제어의-역전ioc-inversion-of-control">1.4 제어의 역전(IoC, Inversion Of Control)</h2>
<p><strong>팩토리</strong></p>
<ul>
<li>객체의 생성 방법을 결정하고 생성한 오브젝트를 돌려주는 일을 하는 오브젝트</li>
<li>오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 역할과 책임을 깔끔하게 분리하려는 목적으로 사용한다</li>
</ul>
<p><strong>제어권의 이전을 통한 제어관계 역전</strong></p>
<ul>
<li><p>제어의 역전에서는 프레임워크 또는 컨테이너와 같이 애플리케이션 컴포넌트의 생성과 관계설정, 사용, 생명주기 관리 등을 관장하는 존재가 필요하다.</p>
<p>  → 교재에서의 예시: DaoFactory</p>
</li>
</ul>
<br>

<h2 id="15-스프링의-ioc">1.5 스프링의 IoC</h2>
<blockquote>
<p>💬 스프링의 핵심을 담당하는 건, 바로 빈 팩토리 또는 애플리케이션 컨텍스트라고 불리는 것이다. 이 두가지는 우리가 만든 DaoFactory가 하는 일을 좀 더 일반화한 것이라고 설명할 수 있다.</p>
</blockquote>
<h3 id="애플리케이션-컨텍스트와-설정정보">애플리케이션 컨텍스트와 설정정보</h3>
<p><strong>빈, Bean</strong></p>
<ul>
<li>오브젝트 단위의 애플리케이션 컴포넌트</li>
<li>스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트</li>
<li>스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트</li>
</ul>
<p><strong>빈 팩토리, Bean Factory</strong></p>
<ul>
<li>빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트</li>
<li>IoC의 기본 기능에 초점을 맞췄을 때 주로 빈 팩토리라는 용어를 사용한다.</li>
</ul>
<p><strong>애플리케이션 컨텍스트</strong></p>
<ul>
<li>IoC 방식을 따라 만들어진 일종의 빈 팩토리</li>
<li>애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업을 담당하는 IoC엔진이라는 의미가 좀 더 부각된다.</li>
</ul>
<br>

<h3 id="애플리케이션-컨텍스트가-ioc-방식의-기능을-제공할-때-사용할-설정정보">애플리케이션 컨텍스트가 IoC 방식의 기능을 제공할 때 사용할 설정정보</h3>
<pre><code class="language-java">@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao() {
        return new UserDao(connectionMaker());
    }

    @Bean
    public ConnectionMaker connectionMaker(){
        return new DConnectionMaker();
    }
}</code></pre>
<ul>
<li><strong>@Configuration 어노테이션</strong><ul>
<li>스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식한다.</li>
<li>애플리케이션 컨택스트가 활용하는 IoC 설정정보이다.</li>
</ul>
</li>
<li><strong>@Bean 어노테이션</strong><ul>
<li>오브젝트를 만들어주는 메소드에 해당 어노테이션을 붙인다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">public class UserDaoTest {
    public static void main(String[] args) throws ClassNotFoundException, 
                 SQLException {
            ApplicationContext context = 
                new AnnotationConfigApplicationContext(DaoFactory.class);
            UserDao dao = context.getBean(&quot;userDao&quot;, UserDao.class);
            ...
}</code></pre>
<ul>
<li>getBean()의 첫 번째 파라미터, “userDao”<ul>
<li>DaoFactory에서 @Bean 어노테이션을 userDao 이름의 메소드에 붙였기 때문에, 이 메소드 이름이 바로 빈의 이름이 된다.</li>
<li>userDao라는 이름의 빈을 가져온다는 것은 DaoFactory의 userDao() 메소드를 호출해서 그 결과를 가져온다고 생각하면 된다.</li>
</ul>
</li>
<li>getBean()의 두 번째 파라미터<ul>
<li>getBean()은 기본적으로 Object 타입을 리턴하기 때문에 매번 오브젝트를 다시 캐스팅해줘야 하는 부담이 있다.</li>
<li>자바 5 이상의 제네릭 메소드 방식을 사용해 getBean()의 두 번째 파라미터에 리턴 타입을 주면 캐스팅 코드를 추가로 작성하지 않아도 된다.</li>
</ul>
</li>
</ul>
<br>

<h2 id="16-싱글톤-레지스트리와-오브젝트-스코프">1.6 싱글톤 레지스트리와 오브젝트 스코프</h2>
<p><strong>오브젝트의 동일성과 동등성</strong></p>
<ul>
<li><code>동일성</code> == 연산자</li>
<li><code>동등성</code> equals() 메서드<ul>
<li>두 개의 각기 다른 오브젝트의 <strong>정보</strong>가 동등한 경우를 말한다.<ul>
<li>동일한 오브젝트는 동등하지만, 그 반대는 항상 참은 아니다.</li>
</ul>
</li>
<li>단, Object의 equals() 메서드는 두 오브젝트이 동일성을 비교한다.</li>
</ul>
</li>
</ul>
<p>스프링 컨텍스트로부터 가져온 오브젝트는 매번 동일한 값을 돌려준다. </p>
<h3 id="161-싱글톤-레지스트리로서의-애플리케이션-컨텍스트">1.6.1 싱글톤 레지스트리로서의 애플리케이션 컨텍스트</h3>
<blockquote>
<p>애플리케이션 컨텍스는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이다.</p>
</blockquote>
<p><strong>왜 스프링은 싱글톤으로 빈을 만들까?</strong></p>
<ul>
<li>스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문이다.</li>
<li>매번 클라이언트에서 요청이 올 때마다 각 로직을 담당하는 오브젝트를 새로 만들어서 사용한다고 하면 부하가 매우 클 것이고, 서버가 감당하기는 힘들어진다.</li>
<li>이런 이유로 서블릿은 서비스 오브젝트로, 대부분 멀티스레드 환경에서 싱글톤으로 동작한다. 서블릿 클래스당 하나의 오브젝트만 만들어두고, 사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해 동시에 사용한다.</li>
</ul>
<br>

<p><strong>싱글톤 패턴의 한계</strong></p>
<ul>
<li>생성자는 private으로 제한되기 때문에 다른 생성자가 없다면 상속이 불가하다. <ul>
<li>객체지향의 장점인 상속과 이를 이용한 다형성을 적용할 수 없고, 객체지향적인 설계의 장점을 적용하기 어렵게 만든다. </li>
</ul>
</li>
<li>싱글톤은 테스트하기 어렵다.<ul>
<li>싱글톤은 만들어지는 방식이 제한적이므로 테스트에서 목 오브젝트로 대체하기 힘들다. </li>
<li>생성자를 통해 사용할 오브젝트를 다이내믹하게 주입하기도 어렵다.</li>
</ul>
</li>
<li>서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.<ul>
<li>서버에서 클래스 로더를 어떻게 구성하냐에 따라 하나 이상의 오브젝트가 만들어질 수 있다.<ul>
<li>여러 개의 JVM에 분산돼서 설치되는 경우에도 각각 독립적으로 오브젝트가 생기므로 싱글톤으로서의 가치가 떨어진다.</li>
</ul>
</li>
</ul>
</li>
<li>싱글톤의 사용은 전역 상태를 만들 수 있다.</li>
</ul>
<br>

<p><strong>대신 스프링은 싱글톤 레지스트리를 제공한다</strong></p>
<ul>
<li>자바의 기본적인 싱글톤 구현 패턴 방식의 단점을 극복하기 위해, 스프링이 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다.</li>
<li>스태틱 메서드와 private 생성자를 사용하지 않아도 평범한 자바 클래스를 싱글톤으로 활용할 수 있게 된다.</li>
</ul>
<br>

<h3 id="162-싱글톤과-오브젝트의-상태">1.6.2 싱글톤과 오브젝트의 상태</h3>
<blockquote>
<p>싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되는 경우에는 상태정보를 내부에 갖고 있지 않은 무상태 방식으로 만들어져야 한다.</p>
</blockquote>
<ul>
<li>상태가 없는 방식으로 클래스를 만들면서 각 요청에 대한 정보를 다루기 위해서는 파라미터와 로컬 변수, 리턴 값 등을 이용하면 된다.</li>
<li>메소드 파라미터나, 메소드 안에서 생성되는 로컬 변수는 매번 새로운 값을 저장할 독립적인 공간이 만들어지기 때문에 싱글톤이라고 해도 여러 스레드가 변수의 값을 덮어쓸 일이 없다.</li>
</ul>
<p><strong>읽기 전용 속성의 정보라면 싱글톤에서 인스턴스 변수로 사용해도 좋다.</strong>
물론 단순한 읽기전용 값이라면 <code>static final</code> 이나 <code>final</code>로 선언하는 편이 낫다.</p>
<ul>
<li>static final<ul>
<li>클래스의 모든 인스턴스에 대해 동일한 값을 갖는다.</li>
</ul>
</li>
<li>final<ul>
<li>개별 인스턴스마다 고유의 값을 갖는다.</li>
<li>인스턴스는 new로 힙에 새로 생성될 때 각 인스턴스에서 설정한 값으로 초기화되기 떄문이다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준/Ruby] 2800번: 괄호 제거]]></title>
            <link>https://velog.io/@yul_ee/%EB%B0%B1%EC%A4%80Ruby-2800%EB%B2%88-%EA%B4%84%ED%98%B8-%EC%A0%9C%EA%B1%B0</link>
            <guid>https://velog.io/@yul_ee/%EB%B0%B1%EC%A4%80Ruby-2800%EB%B2%88-%EA%B4%84%ED%98%B8-%EC%A0%9C%EA%B1%B0</guid>
            <pubDate>Wed, 22 Jan 2025 04:06:39 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/2800">https://www.acmicpc.net/problem/2800</a></p>
<p>아래 예제와 같이 올바른 괄호 쌍을 제거해서 나올 수 있는 서로 다른 식을 사전순으로 출력하는 문제이다.
<img src="https://velog.velcdn.com/images/yul_ee/post/aa1f4310-4aae-42af-a52f-98caa84234b5/image.png" alt=""></p>
<br>

<h2 id="풀이-과정">풀이 과정</h2>
<p>올바른 괄호쌍을 제거하는건 <code>Stack</code> 자료구조를 사용하면 간단히 해결할 수 있다. 문제는 괄호 쌍을 제거해서 나올 수 있는 서로 다른 식을 어떻게 구하느냐였다.</p>
<p>예제 출력 3을 보면 규칙을 파악할 수 있는데, 괄호가 하나 제거된 경우 -&gt; 두 개 제거된 경우 -&gt; 세 개 제거된 경우 순으로 출력된다.</p>
<p>이때 <code>조합</code>을 사용하면 괄호쌍을 제거해서 나올 수 있는 서로 다른 식을 하나도 빠짐없이 구할 수 있다는 것을 알 수 있다.</p>
<br>

<h2 id="풀이">풀이</h2>
<p>조만간 Ruby로 라이브 코딩테스트가 예정되어 있는 관계로 이번 풀이는 루비로 진행해보았다.</p>
<ul>
<li><p><code>count</code></p>
<ul>
<li>입력받은 수식에 존재하는 괄호쌍의 개수를 의미한다.</li>
</ul>
</li>
<li><p><code>findPair()</code></p>
<ul>
<li>입력받은 수식에서 알맞은 괄호쌍들을 찾아낸다.</li>
<li>Stack 자료구조의 특성을 활용했다.</li>
</ul>
</li>
<li><p><code>combination 활용부</code></p>
<ul>
<li>findPair()에서 찾아낸 괄호쌍들에 대해, 1부터 count 까지의 개수만큼 조합을 구한다.</li>
<li>아래처럼 알맞은 괄호쌍들의 인덱스가 조합으로 구성되어 있는 것을 확인할 수 있다.<pre><code>[[[6, 10]], 
[[3, 11]], 
[[0, 12]], 
[[6, 10], [3, 11]], 
[[6, 10], [0, 12]], 
[[3, 11], [0, 12]], 
[[6, 10], [3, 11], [0, 12]]]</code></pre></li>
</ul>
</li>
<li><p><code>output()</code></p>
<ul>
<li>입력받은 수식에 대해서, 위 조합을 한줄씩 순회하며 괄호쌍과 일치하는 인덱스이면 pass하고, 일치하지 않으면 수식으로 출력하기 위해 <code>result</code> 배열에 저장한다.</li>
</ul>
</li>
<li><p><code>사전순 정렬</code></p>
<ul>
<li>조합으로 구한 모든 수식은 result 배열에 담겨 있다.</li>
<li>문제 출력 조건을 만족하기 위해, 이 배열을 정렬하는 과정이 필요하다.</li>
</ul>
</li>
</ul>
<br>


<pre><code class="language-ruby">def findPair(input)
  tmp = []
  stack = []

  for i in 0...input.length do
    if input[i] == &quot;(&quot;
      tmp.push(i)
    elsif input[i] == &quot;)&quot;
      open = tmp.pop()
      close = i
      stack.push([open, close])
    end
  end
  return stack
end

def output(input, arr)
  result = []

  arr.each do |pair|
    idx = Array.new(input.length, 0)

    pair.each do |a, b|
      idx[a] = 1
      idx[b] = 1
    end

    ans = &quot;&quot;
    for i in 0...input.length do
      if idx[i] == 1
        next
      end
      ans += input[i]
    end
    result &lt;&lt; ans
  end

  return result
end

input = gets.chomp
count = input.count(&quot;(&quot;)
stack = findPair(input)

comb = []
for i in 1..count do
  comb.concat(stack.combination(i).to_a)
end

result = output(input, comb)
result.uniq.sort.each {|result| puts result}</code></pre>
<br>

<h3 id="사담">사담</h3>
<p>조건문, 반복문 끝날 때마다 end 붙여줘야 되는거 진짜 열받는다.. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준/Python] 7662번: 이중 우선순위 큐]]></title>
            <link>https://velog.io/@yul_ee/%EB%B0%B1%EC%A4%80-7662%EB%B2%88-%EC%9D%B4%EC%A4%91-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%ED%81%90</link>
            <guid>https://velog.io/@yul_ee/%EB%B0%B1%EC%A4%80-7662%EB%B2%88-%EC%9D%B4%EC%A4%91-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%ED%81%90</guid>
            <pubDate>Wed, 15 Jan 2025 06:05:39 GMT</pubDate>
            <description><![CDATA[<br>

<p><img src="https://velog.velcdn.com/images/yul_ee/post/1b9c175b-c3db-499d-ac4e-4ef19d10cf88/image.png" alt=""></p>
<br>

<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/7662">https://www.acmicpc.net/problem/7662</a></p>
<p>(요약)</p>
<ul>
<li>삽입 명령 시, 우선순위 큐에 데이터를 삽입한다.</li>
<li>삭제 명령 시, 우선순위 큐에서 최소값 혹은 최대값 데이터를 삭제한다.</li>
<li>이때 Q에 적용할 연산의 개수를 나타내는 정수 k (k ≤ 1,000,000)</li>
</ul>
<Br>

<h2 id="풀이-과정">풀이 과정</h2>
<p>처음엔 우선순위 큐를 하나만 가지고 운영하면서 최대값/최소값 명령에 따라 그때그때 최소힙/최대힙으로 변환하는 작업을 고려했다. 하지만 이 경우 최소힙/최대힙으로 재정렬하는 작업이 반복되어 비효율적이고, 시간제한도 지키기 힘들다.</p>
<p>따라서 <strong>힙 자체를 최소힙, 최대힙 두개로 운영</strong>하여 최소값 삭제 명령시 최소힙에서 heappop()을 호출, 최대값 삭제 명령시 최대힙에서 heappop()을 호출하는 방향으로 구현하고자 했다.</p>
<p>이때 <strong>최소힙과 최대힙의 동기화</strong>를 고려해야 한다. (최소힙에서 이미 삭제한 데이터를 최대힙에서 최대값으로 찾는 오류 방지 차원.. 그 반대도 마찬가지)</p>
<p>나는 <code>deleted</code> 리스트를 사용해 이미 삭제된 데이터인지 표시해두고, 힙에서 이미 삭제된 데이터는 제외하고 최대값/최소값을 판별할 수 있도록 아래와 같이 코드를 구성했다.</p>
<br>

<h2 id="풀이">풀이</h2>
<pre><code class="language-python">import sys
input = lambda: sys.stdin.readline().rstrip()
import heapq

t = int(input())
for _ in range(t):
    k = int(input())
    maxH = []
    minH = []
    deleted = [False] * k

    for i in range(k):
        comm = list(map(str, input().split()))
        c = comm[0]
        n = int(comm[1])

        if c == &quot;I&quot;:
            heapq.heappush(maxH, (-n, i))
            heapq.heappush(minH, (n, i))

        else:
            if n == 1: #최대값 삭제
                while maxH and deleted[maxH[0][1]]:
                    heapq.heappop(maxH)
                if maxH:
                    deleted[maxH[0][1]] = True
                    heapq.heappop(maxH)
            else: #최소값 삭제
                while minH and deleted[minH[0][1]]:
                    heapq.heappop(minH)
                if minH:
                    deleted[minH[0][1]] = True
                    heapq.heappop(minH)

    while maxH and deleted[maxH[0][1]]:
        heapq.heappop(maxH)
    while minH and deleted[minH[0][1]]:
        heapq.heappop(minH)

    if maxH and minH:
        print(-maxH[0][0], minH[0][0])
    else:
        print(&quot;EMPTY&quot;)</code></pre>
<br>

<h2 id="번외">번외</h2>
<blockquote>
<p>내장함수 <code>min</code> <code>max</code>를 사용하면 안될까?</p>
</blockquote>
<p>(1) 의사코드</p>
<pre><code class="language-python">k = int(input())
h = []

for i in range(k):
    comm, n = input().split()

    if comm == &quot;I&quot;: # (1) 데이터 삽입 
        h.append(n)

    else:
        if n == 1: 
            # (2) 최대값 삭제
            maxN = max(h)
            h.remove(maxN)
        else:
            # (3) 최소값 삭제
            minN = min(h)
            h.remove(minN)</code></pre>
<ul>
<li>max(), min() 연산은 리스트를 순차 탐색하면서 최대값/최소값을 찾으므로 O(n)의 시간복잡도를 갖는다.</li>
<li>remove() 연산은 리스트를 순차 탐색하면서 특정값을 삭제하므로 O(n)의 시간복잡도를 갖는다.</li>
</ul>
<p>(2) 시간 복잡도 분석</p>
<ul>
<li>삽입 명령: O(1)</li>
<li>삭제 명령: O(k*k)<ul>
<li>최대/최대값 탐색에 O(k)</li>
<li>삭제 작업에 O(k) 소요</li>
</ul>
</li>
<li>최악의 경우(k=1,000,000), O(1,000,000^2) = O(10^12) 연산이 필요</li>
<li>1초에 1억회 연산이라고 어림잡을 때 10,000초가 소요되어 시간초과</li>
</ul>
<p>(3) 결론
당연히 안된다. (^^;)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준/Java] 22859번: HTML 파싱]]></title>
            <link>https://velog.io/@yul_ee/%EB%B0%B1%EC%A4%80-22859%EB%B2%88-HTML-%ED%8C%8C%EC%8B%B1</link>
            <guid>https://velog.io/@yul_ee/%EB%B0%B1%EC%A4%80-22859%EB%B2%88-HTML-%ED%8C%8C%EC%8B%B1</guid>
            <pubDate>Mon, 18 Nov 2024 16:03:07 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/22859">https://www.acmicpc.net/problem/22859</a></p>
<br>

<h2 id="풀이-과정">풀이 과정</h2>
<ul>
<li>정규표현식으로 풀었다.</li>
<li>정규표현식에 익숙치 않아서 아래 링크들을 많이 참고하면서 공부하듯 품<ul>
<li><a href="https://blex.me/@mildsalmon/22859%EB%B2%88-html-%ED%8C%8C%EC%8B%B1">풀이 참고</a></li>
<li><a href="https://devlog.jsyoo5b.net/ko/posts/regex/basic/">개념 참고</a></li>
</ul>
</li>
</ul>
<br>

<h2 id="새로-배운-개념">새로 배운 개념</h2>
<h3 id="-사용법">.*? 사용법</h3>
<ul>
<li><code>.</code> : 모든 문자</li>
<li><code>*</code> : 0번 이상 반복</li>
<li><code>?</code> : 비탐욕적 매칭 (가장 짧은 범위를 매칭)<br>

</li>
</ul>
<h3 id="와-의-차이는-뭘까">.*와 .*?의 차이는 뭘까</h3>
<pre><code class="language-html">&lt;p&gt;Text1&lt;/p&gt;&lt;p&gt;Text2&lt;/p&gt;</code></pre>
<ul>
<li><code>&lt;p&gt;.*&lt;/p&gt;</code> : (탐욕적 매칭) 최대 범위를 매칭한다.<ul>
<li>&lt;p&gt;Text1&lt;/p&gt;&lt;p&gt;Text2&lt;/p&gt; 가 전부 매칭된다.</li>
</ul>
</li>
<li><code>&lt;p&gt;.*?&lt;/p&gt;</code> : (비탐욕적 매칭) 최소 범위를 매칭한다.<ul>
<li>&lt;p&gt;Text1&lt;/p&amp;gt 까지만 매칭된다.</li>
</ul>
</li>
</ul>
<br>

<h2 id="풀이">풀이</h2>
<h3 id="파이썬-풀이">파이썬 풀이</h3>
<pre><code class="language-python">import sys
import re
input = lambda: sys.stdin.readline().rstrip()

html = input()

# 1. main parsing
s = len(&#39;&lt;main&gt;&#39;)
e = len(&#39;&lt;/main&gt;&#39;)
html = html[s : -e]

# 2. div parsing
html = re.sub(r&#39;&lt;div +title=&quot;([\w ]*)&quot;&gt;&#39;, r&#39;title : \1\n&#39;, html)
html = re.sub(r&#39;&lt;/div&gt;&#39;, &#39;&#39;, html)

# 3. p parsing
html = re.sub(r&#39;&lt;p&gt;(.*?)&lt;/p&gt;&#39;, r&#39;\1\n&#39;, html)

# 4. p parsing - 모든 태그 지우기
html = re.sub(r&#39;&lt;([\w /]*)&gt;&#39;, &#39;&#39;, html)

# 5. p parsing - 맨 앞, 맨 뒤 공백 제거
html = re.sub(r&#39; ?\n ?&#39;, r&#39;\n&#39;, html)

# 6. p parsing - 공백이 2번 이상 나타나면 하나로 대체
html = re.sub(r&#39; {2,}&#39;, &#39; &#39;, html)

print(html)</code></pre>
<br>

<h3 id="자바-풀이">자바 풀이</h3>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String html = br.readLine();

        // 1. main parser
        int s = &quot;&lt;main&gt;&quot;.length();
        int e = &quot;&lt;/main&gt;&quot;.length();
        html = html.substring(s, html.length() - e);

        // 2. div parser
        html = html.replaceAll(&quot;&lt;div +title=\&quot;([\\w ]*)\&quot;&gt;&quot;, &quot;title : $1\n&quot;);
        html = html.replaceAll(&quot;&lt;/div&gt;&quot;, &quot;&quot;);

        // 3. p parser
        html = html.replaceAll(&quot;&lt;p&gt;(.*?)&lt;/p&gt;&quot;, &quot;$1\n&quot;);

        // 4. p parser - remove all tag
        html = html.replaceAll(&quot;&lt;([\\w /]*)&gt;&quot;, &quot;&quot;);

        // 5. p parser - trim
        html = html.replaceAll(&quot; ?\n ?&quot;, &quot;\n&quot;);

        // 6. p parser - two space -&gt; one space
        html = html.replaceAll(&quot; {2,}&quot;, &quot; &quot;);

        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        bw.write(html);
        bw.flush();
        bw.close();
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[모니터링 자동화를 위한 New Relic 도입기]]></title>
            <link>https://velog.io/@yul_ee/%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%89%B4%EB%A0%90%EB%A6%AD-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@yul_ee/%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%89%B4%EB%A0%90%EB%A6%AD-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Fri, 11 Oct 2024 08:20:02 GMT</pubDate>
            <description><![CDATA[<p>그동안 개발 환경에서 EC2 서버에 문제가 생기거나, 혹은 code deploy를 통한 배포 자동화 과정에서 문제가 발생할 때 바로 상황을 파악하지 못했던 적이 종종 있다. 어떤 방식으로든 개발자에게 알림이 가게끔 환경을 구축했다면 좋았겠지만, 사실 개발 단계에서는 실시간으로 알림을 받아 볼 필요성을 크게 느끼지 못했다. 그렇지만 프로덕션 환경에서는 이 문제를 분명 개선할 필요가 있었다.
</br></p>
<h2 id="도입기">도입기</h2>
<p>우리에게 필요한 건 자동화된 모니터링 도구였다. 더 이상 배포가 잘 되었는지, 서버가 잘 돌아가고 있는지, 장애가 난 포인트는 없는지 등을 직접 확인하지 않아도 되게끔 만드는 것이 제일 중요했다. &#39;무소식이 희소식이겠거니 ~&#39; 하면서 있다가 모니터링 도구가 실시간으로 문제를 알려주면 우리는 빠르게 문제를 해결해서 사용자들이 안정적으로 서비스를 이용할 수 있도록 환경을 마련해야 했다! 
이를 도와주는 자동화된 모니터링 도구로는 sentry, data dog, prometheus, cloud watch 등으로 매우 다양한데, 우린 그 중에서도 <code>new relic</code>을 사용하기로 했다. 사실 뉴렐릭은 sentry, cloud watch, prometheus 등에 비해서 레퍼런스가 굉장히 적기도 했고 올해 5월인가에는 뉴렐릭 한국 지사가 철수하기도 하는 등 &#39;도입하지 않을&#39; 이유가 더욱 많아보였다. 그럼에도 소수의 레퍼런스 자료에서(<a href="https://techblog.gccompany.co.kr/%EC%97%AC%EA%B8%B0%EC%96%B4%EB%95%8C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-new-relic-%EC%9D%B4%EC%95%BC%EA%B8%B0-dad36583dec4">여기어때 기술블로그</a>) 뉴렐릭의 만족도가 커 보여 한번쯤은 경험해보고 싶다는 생각이 들었고, 또 무엇보다 현재 활동 중인 소프트웨어 마에스트로의 담당 멘토님께서 뉴렐릭을 써볼 것을 강추❗️하셨기 때문에 그냥 믿고 써봐야겠다 싶었다.</p>
</br>

<h2 id="✋-들어가기-전에">✋ 들어가기 전에</h2>
<blockquote>
<p>모니터링에는 pull과 push 방식이 있다</p>
</blockquote>
<p>뉴렐릭 연동 과정에 대한 마땅한 참고자료가 없었고 공식문서도 개인적으로는 이해하기에 부족한 점이 있었기 때문에 의존성만 설치하면 끝나는건지, 아님 배포 서버에서 뭔가를 더 설정해줘야 하는건지 기본적인 설정법조차 와닿지 않았다. 
이제와서 돌아보니 이 &#39;모니터링 방식&#39;에 대한 개념이 부족해서 이리저리 헤맨 것 같다. 간단하게 설명하면 pull 방식의 경우 모니터링 도구가 메트릭을 자동으로 수집(pull)해서 에러 상황을 분석해주고, push 방식의 경우 코드 상에서 직접 특정 포인트에서 데이터가 수집되도록 설정해 보내야(push)한다. sentry가 대표적인 push 방식으로 동작하며 new relic의 경우 pull 방식으로 동작한다.</p>
<p>new relic이 pull 방식으로 동작하기 위해서는 애플리케이션을 실행할 때 new relic agent가 애플리케이션에 통합하도록 설정해주는 과정이 필요하다. 또한 애플리케이션이 실행 중일 때 뉴렐릭 에이전트도 같이 돌아가는 것이기 때문에, 새 버전을 배포하는 과정에서 기존에 돌아가던 애플리케이션을 kill 해버리면 뉴렐릭 에이전트도 더 이상 모니터링을 하지 못하게 된다. 따라서 새 버전을 배포할 때마다 new relic agent를 애플리케이션에 통합되도록 해줘야 한다. </p>
</br>

<h2 id="뉴렐릭이-제공하는-기능">뉴렐릭이 제공하는 기능</h2>
<ul>
<li><del>뉴렐릭은 APM 모니터링 기능에 매우 특화되어 있다. ~</del></li>
</ul>
</br>

<h2 id="학생용-뉴렐릭-사용하기">학생용 뉴렐릭 사용하기</h2>
<p>뉴렐릭은 상용 서비스로 돈을 내고 사용하게 되는데 사용량에 따라 가격이 매겨지고 이게 상당히 비싼 것으로 알려져있다. 공식 사이트에서는 사용한 만큼 비용을 지불하는 걸 큰 장점처럼 어필하고 있긴 한데, 사용자 입장에선 이게 더 폭력적으로(😅) 다가오긴 한다.
<img src="https://velog.velcdn.com/images/yul_ee/post/616635ed-2cc0-44be-b584-8325730fae81/image.png" alt=""></p>
<p>프라이싱 모델은 아래와 같이 구성되어있다. 무료버전으로 사용해도 괜찮을 것 같다는 생각이 들었는데, 럭키🍀하게도 학생의 경우 무료로 더 많은 혜택을 받을 수 있게끔 되어있었다. 애플리케이션 모니터링 + 인프라 모니터링 등 30개 기능을 이용할 수 있고, 매월 500GB의 데이터를 사용할 수 있고, 플랫폼 사용자도 3명까지 등록할 수 있다!! 
다행히(?) 아직 대학생 신분이라 <a href="https://newrelic.com/kr/students">학생용 뉴렐릭</a> 계정을 만들었다.
<img src="https://velog.velcdn.com/images/yul_ee/post/66e69f73-5173-4a9a-91a5-84ac8df0c531/image.png" alt=""></p>
</br>

<h2 id="프로젝트에-통합하기">프로젝트에 통합하기</h2>
<ol>
<li>APM(Application Monitoring) - 언어 선택
<code>자바</code>로 개발중이라 모니터링 대상 언어는 자바로 선택했다.
<img src="https://velog.velcdn.com/images/yul_ee/post/9d66f125-e160-4644-b8cf-d452a69744e3/image.png" alt=""></li>
<li>설치 방식
의존성을 추가할건지(gradle, maven), 아니면 운영중인 서버에 직접 설치할 건지(On a host) 선택하면 된다. 나의 경우에는 프로젝트에 의존성을 추가하는게 이후에 설정하거나 커스텀하기에 더 편할 것 같아 gradle을 선택했다. 
<img src="https://velog.velcdn.com/images/yul_ee/post/31d270f5-b81c-4d5b-8a14-b94ab34e5069/image.png" alt=""></li>
<li>개발 환경 선택하기
linux, window 중에 선택하고 사용하고 있는 프레임워크를 선택하면 된다. 다음 단계에서 설정파일을 제공해주는데 거기에 사용되는 것 같다.
<img src="https://velog.velcdn.com/images/yul_ee/post/e7e45826-6aa6-496c-a0ce-d0843c84d5b0/image.png" alt=""></li>
<li>buil.gradle에 의존성 추가
아래 예시로 보여준 build.gradle을 복사해서 프로젝트 build.gradle에 추가한다. 내가 사용하는 버전에서는 조금 문법이 달라져 추가 수정이 필요했다.
<img src="https://velog.velcdn.com/images/yul_ee/post/810dfd1c-569a-4f8f-8b8a-4c8b2bdcc994/image.png" alt=""><pre><code>tasks.register(&#39;downloadNewrelic&#39;, Download) {
 mkdir &#39;newrelic&#39;
 src &#39;https://download.newrelic.com/newrelic/java-agent/newrelic-agent/current/newrelic-java.zip&#39;
 dest file(&#39;newrelic&#39;)
}
</code></pre></li>
</ol>
<p>tasks.register(&#39;unzipNewrelic&#39;, Copy) {
    from zipTree(file(&#39;newrelic/newrelic-java.zip&#39;))
    into rootDir
}</p>
<pre><code>5. 설정파일(newrelic.yml) 세팅
4 단계를 거치고 나면 프로젝트의 루트 디렉터리에 newrelic 폴더가 생긴다. newrelic 폴더 안에 newrelic.yml 파일이 있는데 여기에 4의 화면에 보이는 newrelic.yml 코드를 그대로 복사해서 붙여넣으면 된다. 주의할 건 newrelic.yml에 `lisence_key`가 그대로 노출되어 있다는 건데, 이걸 환경변수로 등록해주는게 좋다.
![](https://velog.velcdn.com/images/yul_ee/post/12ffb03a-990b-4b61-9ea7-b0b7ed3a4a62/image.png)
6. 애플리케이션 실행 시 new relic agent를 통합
여기가 조금 헷갈렸는데, 처음에는 code deploy의 라이프 사이클 중 Application Start단계에서 new relic agent를 실행시키기 위한 스크립트를 하나 새로 생성해야 하는건가 싶었다. 결론적으로 그건 아니었고 배포 자동화가 이뤄지고 있는 경우에는 새 애플리케이션을 실행하는 스크립트를 아래 명령어로 보완만 해주면 됐다. (뉴렐릭이 어떻게 동작하는건지 이해가 부족했음)
![](https://velog.velcdn.com/images/yul_ee/post/a12a140f-e20e-4460-98f8-07268944ef17/image.png)
우리 서비스는 start.sh에 새 애플리케이션을 실행하는 스크립트를 짜뒀기 때문에 아래와 같이 start.sh을 보완했다. 
(기존)</code></pre><p>  nohup java -jar $JAR</p>
<pre><code>  (보완)</code></pre><p>  nohup java -javaagent:$NEW_RELIC_JAR_FILE -jar $JAR</p>
<pre><code>7. (optional) 인프라와 연동 
그 다음 과정부터는 선택적으로 진행하면 되는데 왜인지 모르게 계속 실패가 떠서 일단은 APM 모니터링 연동까지만 진행했다. 

&lt;/br&gt;

## 짜잔
여기까지 진행하면 이제 아래와 같이 모니터링 내용을 확인할 수 있다. 사실 좋은 도구가 있어도 해석하는 능력이 없으면 무용지물인데 지금 좀 그런 기분이 든다.. 😓 이제는 어떻게 데이터를 해석할건지에 대한 공부를 해보고, 어떤 경우에 알림을 받을건지 정책을 정하기 위해서 또 이리저리 알아봐야 할 것 같다 ~! 
![](https://velog.velcdn.com/images/yul_ee/post/dec6e6af-626a-4c20-b5e6-955e9da022b0/image.png)


</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] CodeDeploy 배포자동화 오류 해결하기 (*오류코드: ScriptFailed)]]></title>
            <link>https://velog.io/@yul_ee/AWS-CodeDeploy-%EB%B0%B0%ED%8F%AC%EC%9E%90%EB%8F%99%ED%99%94-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-%EC%98%A4%EB%A5%98%EC%BD%94%EB%93%9C-ScriptFailed</link>
            <guid>https://velog.io/@yul_ee/AWS-CodeDeploy-%EB%B0%B0%ED%8F%AC%EC%9E%90%EB%8F%99%ED%99%94-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-%EC%98%A4%EB%A5%98%EC%BD%94%EB%93%9C-ScriptFailed</guid>
            <pubDate>Sun, 22 Sep 2024 11:44:08 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@yul_ee/AWS-CodeDeploy-%EB%B0%B0%ED%8F%AC%EC%8B%A4%ED%8C%A8-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0">가장 최근에 작성했던 글</a>에서 서버 용량이 가득 차 자동 배포가 이뤄지지 않는 문제를 서버 용량 증설을 통해 해결했던 경험을 기록했었다. 이때 문제가 되고 있는 서버 용량을 늘렸으니 재배포를 시도했을 때 성공할 것이라 기대했지만, 다른 문제로 인해서 또 배포가 실패하는 이슈가 있어 여기에 대해서도 기록해보고자 한다 🤯</p>
</br>

<h2 id="codedeploy-라이프-사이클">CodeDeploy 라이프 사이클</h2>
<p>이번 배포가 실패한 로그를 분석하기 전에 CodeDeploy의 라이프 사이클을 이해하는 시간이 필요했다. CodeDeploy는 아래와 같은 순서로 이벤트 훅을 동작시켜 배포를 자동화해주고 있다. 이때 연하늘색으로 표시된 이벤트의 경우에는 해당 이벤트가 발생했을 때 어떤 훅을 동작시킬 것인지 <code>appspec.yml</code>에 유저가 커스텀하게 정의할 수 있도록 지원한다. 이 중 설명이 필요한 몇 가지에 이벤트에 대해서만 간단하게 언급하고자 한다.</p>
<ul>
<li><code>install</code> 배포하려는 애플리케이션 파일 및 의존성을 EC2 인스턴스에 설치한다.</li>
<li><code>AfterInstall</code> 설치가 완료된 애플리케이션 파일을 실행시키기 전에 해야 하는 작업이 있다면 해당 단계에서 수행한다.</li>
<li><code>ApplicationStart</code> 애플리케이션을 실행하는 작업을 수행한다.</li>
</ul>
<p align="center"><img src="https://velog.velcdn.com/images/yul_ee/post/3c70a5dc-1a6c-439a-b0ac-4d16a7b2d1a7/image.png
" width="300" height="300">

</br>

<p>우리 서비스의 경우에는 아래와 같이 <code>appspec.yml</code>이 작성되어 있다. </p>
<pre><code>hooks:
  AfterInstall:
    - location: scripts/stop.sh
      timeout: 60
      runas : root
  ApplicationStart:
    - location: scripts/start.sh
      timeout: 60
      runas : root</code></pre></br>

<p>이때 AfterInstall 단계에서는 아래와 같이 작성한 stop.sh을 실행시켜 새 애플리케이션을 실행하기 전에 기존에 배포되어있는 애플리케이션의 프로세스를 kill하는 작업을 한다.</p>
<pre><code>#!/bin/bash

ROOT_PATH=&quot;/home/ubuntu/freebe-backend&quot;
JAR=&quot;$ROOT_PATH/application.jar&quot;
STOP_LOG=&quot;$ROOT_PATH/stop.log&quot;
SERVICE_PID=$(pgrep -f $JAR)

if [ -z &quot;$SERVICE_PID&quot; ]; then
  echo &quot;서비스 NouFound&quot; &gt;&gt; $STOP_LOG
else
  echo &quot;서비스 종료 &quot; &gt;&gt; $STOP_LOG
  kill &quot;$SERVICE_PID&quot;
fi
</code></pre></br>

<h2 id="배포-실패-로그-분석하기">배포 실패 로그 분석하기</h2>
<p>이번 배포실패 로그를 살펴보면 EC2 인스턴스에 애플리케이션 설치까지는 성공적으로 수행되었지만, AfterInstall 훅에서 stop.sh 스크립트를 실행하던 중 오류가 난 것을 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/yul_ee/post/c9101dd9-5f39-4671-b3cf-eb6a82c6e39a/image.png" alt=""></p>
<p>stop.sh 스크립트 중에서 기존에 실행중이던 프로세스를 kill하는 과정에서 arguments must not be process or job IDs 로그가 찍힌 것을 보면 아마 kill 명령어와 관련해 문제가 발생하고 있는 것 같다.
<img src="https://velog.velcdn.com/images/yul_ee/post/f3cb00ad-caea-4bc3-bb82-8d442eba5f97/image.png" alt=""></p>
</br>

<h2 id="실패-원인-가정하기">실패 원인 가정하기</h2>
<p>그렇다면 kill 명령어를 수행하는데 있어 발생할 수 있는 문제는 무엇이 있을까? 가장 먼저 떠올린 것은 권한 문제였다. 터미널에서 직접 kill 명령어를 수행하려고 하면 <code>Operation not permitted</code>가 출력되는 것을 확인할 수 있다.</p>
<p>왜냐하면 현재 내가 접속한 일반 사용자 계정으로는 root 권한으로 실행되는 application.jar 프로세스를 kill할 권한이 없기 때문이다. 이와 유사하게 CodeDeploy 에이전트에게도 root 권한이 없어서 kill 명령이 제한되고 있는 건 아닐까라는 생각을 하게 되었다. 하지만 확인해본 결과 CodeDeploy agent도 root 권한으로 실행되고 있었기 때문에 권한 문제는 아니었다.
<img src="https://velog.velcdn.com/images/yul_ee/post/8dd2c1a9-2392-4c46-a965-f935046762c2/image.png" alt=""></p>
</br>

<p>그렇다면 kill 명령이 제대로 입력되지 않고 있는건 아닐까? 아래 stop.sh 스크립트를 다시 살펴보면 <code>pgrep -f application.jar</code>로 실행중인 프로세스를 <code>SERVICE_PID</code> 변수에 저장하고 <code>kill $SERVICE_PID</code>를 수행하게 되는데 SERVICE_PID에 pid가 제대로 저장되지 않고 있을 수 있다는 생각이 들었다. </p>
<pre><code>#!/bin/bash

ROOT_PATH=&quot;/home/ubuntu/freebe-backend&quot;
JAR=&quot;$ROOT_PATH/application.jar&quot;
STOP_LOG=&quot;$ROOT_PATH/stop.log&quot;
SERVICE_PID=$(pgrep -f $JAR)

if [ -z &quot;$SERVICE_PID&quot; ]; then
  echo &quot;서비스 NouFound&quot; &gt;&gt; $STOP_LOG
else
  echo &quot;서비스 종료 &quot; &gt;&gt; $STOP_LOG
  kill &quot;$SERVICE_PID&quot;
fi</code></pre></br>

<p>그런데 터미널에서 직접 실행중인 프로세스를 찾아보니, 왜인지는 모르겠지만 application.jar 프로세스가 두 개가 돌아가고 있는 것을 발견할 수 있었다. 
<img src="https://velog.velcdn.com/images/yul_ee/post/9de98e14-1218-46cf-9bd3-4d5a58f1d26c/image.png" alt=""></p>
<p>186470, 186551 두 프로세스가 시작된 시간을 확인해보니 9월 12일에 20분 간격을 두고 실행된 것을 확인할 수 있었는데, 두번째 프로그램이 배포된 이후부터 실행중인 프로세스가 두 개가 되면서 그 다음 배포 시도부터는 stop.sh이 여러 개의 프로세스를 kill하지 못하게 되어 배포에 실패한게 아닌가 라는 추측을 해보았다. 왜냐하면 기존 stop.sh 스크립트는 실행중인 프로세스가 하나인 경우를 가정하고 이미 존재하는 하나의 프로세스를 종료시키도록 작성되어 있기 때문이다. </p>
<p>그렇지만 스크립트 실행 자체가 실패하는 것은 이해가 가지 않았던 것이, 실행중인 프로세스가 여러개일 때 <code>SERVICE_PID</code> 변수에 기존에 실행중인 여러개의 프로세스 아이디가 공백으로 구분되어 저장되므로(ex: kill 186470 192838) kill 명령을 실행했을 때 가장 맨 앞에있는 프로세스(186470) 한개는 kill 되게끔 동작할 것이기 때문이다.</p>
</br>

<h2 id="조치하기">조치하기</h2>
<p>이때까지는 정확한 실패 원인을 파악하지는 못했지만, 예상치 못하게 실행중인 프로세스가 두 개였기 때문에 이를 적절하게 처리하지 못해서 배포에 실패했을거라는 합리적 의심을 하게 되었다. 그래서 실행중인 프로세스가 여러개일 예외상황까지 고려해서 stop.sh에서 실행중인 <strong>모든</strong> 프로세스를 죽일 수 있도록 스크립트를 수정했다. 또 로그를 구체화 해서 앞으로는 어디서 오류가 났는지 쉽게 파악할 수 있도록 했다.</p>
<pre><code>#!/bin/bash

ROOT_PATH=&quot;/home/ubuntu/freebe-backend&quot;
JAR=&quot;$ROOT_PATH/application.jar&quot;
STOP_LOG=&quot;$ROOT_PATH/stop.log&quot;
NOW=$(date &quot;+%Y %b %d %a %H:%M:%S&quot;)

echo &quot;--------------------------------------------------&quot; &gt;&gt; $STOP_LOG

# 실행 중인 모든 프로세스의 PID를 가져옴
SERVICE_PIDS=$(pgrep -f $JAR)

if [ -z &quot;$SERVICE_PIDS&quot; ]; then
  echo &quot;[$NOW] 실행 중인 기존 프로세스 없음&quot; &gt;&gt; $STOP_LOG
else
  echo &quot;[$NOW] 기존 프로세스 종료 시도: PID $SERVICE_PIDS&quot; &gt;&gt; $STOP_LOG

  for PID in $SERVICE_PIDS; do
    echo &quot;[$NOW] kill $PID 실행 중&quot; &gt;&gt; $STOP_LOG
    kill &quot;$PID&quot;
    sleep 1

    if kill -0 &quot;$PID&quot; 2&gt;/dev/null; then
      echo &quot;[$NOW] 프로세스 강제 종료 시도: PID $PID&quot; &gt;&gt; $STOP_LOG
      kill -9 &quot;$PID&quot;
      echo &quot;[$NOW] 프로세스 강제 종료됨: PID $PID&quot; &gt;&gt; $STOP_LOG
    else
      echo &quot;[$NOW] 프로세스가 성공적으로 종료됨: PID $PID&quot; &gt;&gt; $STOP_LOG
    fi
  done
fi</code></pre></br>

<p>이렇게 스크립트를 수정한 뒤에 다시 배포를 시도했더니 stop.log 파일에서 아래와 같이 스크립트 실행 과정을 트래킹할 수 있게 되었다. 여기서 주목해야 했던 것은 <code>SERVICE_PID</code> 변수에 pid 두개가 공백으로 구분되는 것이 아니라 개행으로 구분되고 있다는 점이었다. </p>
<pre><code>[Sun Sep 22 16:39:48 2024] 기존 프로세스 종료 시도: PID 186470
260650
[Sun Sep 22 16:39:48 2024] kill 186470 실행 중
[Sun Sep 22 16:39:48 2024] 프로세스 강제 종료 시도: PID 186470
[Sun Sep 22 16:39:48 2024] kill 260650 실행 중
[Sun Sep 22 16:39:48 2024] 프로세스가 성공적으로 종료됨: PID 260650</code></pre><p><em><strong>즉, 기존의 stop.sh스크립트로는 <code>kill 186470/n 26065</code> 으로 실행되었기 때문에 명령 자체가 잘못 입력되어 arguments must be process or job IDs 오류가 나면서 그동안 배포에 실패하지 않았을까 싶다.</strong></em> 하지만 이번에 수정한 스크립트에서는 kill 명령을 수행할 때 for문을 돌면서 pid를 꺼내와 하나의 프로세스만 죽이게끔 동작하기 때문에 kill 명령어를 수행할 때 유사한 이슈가 발생하지 않을 것이다.</p>
</br>

<p>또한 이번처럼 두 개 이상의 프로세스가 동시에 실행되고 있는 예외 상황을 아예 만들지 않기 위해서 애플리케이션 실행을 담당하는 start.sh 스크립트도 아래와 같이 보완해보았다. stop.sh 스크립트가 예상대로 실행되지 않아서 새 애플리케이션을 실행하기 전에 기존에 존재하는 프로세스를 종료시키지 못했다면, 새 애플리케이션을 실행하지 않고 배포가 실패하게끔 유도하고 관련 로그를 start.log에 남기도록 해두었다.</p>
<pre><code>#!/bin/bash

# 환경 변수 로드
set -o allexport
source /home/ubuntu/freebe-backend/.env
set +o allexport

# 경로 및 파일 설정
ROOT_PATH=&quot;/home/ubuntu/freebe-backend&quot;
JAR=&quot;$ROOT_PATH/application.jar&quot;

APP_LOG=&quot;$ROOT_PATH/application.log&quot;
ERROR_LOG=&quot;$ROOT_PATH/error.log&quot;
START_LOG=&quot;$ROOT_PATH/start.log&quot;
NOW=$(date &quot;+%Y %b %d %a %H:%M:%S&quot;)

echo &quot;--------------------------------------------------&quot; &gt;&gt; $START_LOG
SERVICE_PIDS=$(pgrep -f $JAR)

# 실행 중인 프로세스가 있는지 확인
if [ ! -z &quot;$SERVICE_PIDS&quot; ]; then
  echo &quot;[$NOW] 이미 실행 중인 프로세스가 있습니다: PID $SERVICE_PIDS&quot; &gt;&gt; $START_LOG
  exit 1 
fi

# JAR 파일 복사
echo &quot;[$NOW] $JAR 복사 중...&quot; &gt;&gt; $START_LOG
if cp $ROOT_PATH/build/libs/freebe-0.0.1-SNAPSHOT.jar $JAR; then
  echo &quot;[$NOW] JAR 파일 복사 완료&quot; &gt;&gt; $START_LOG
else
  echo &quot;[$NOW] JAR 파일 복사 실패&quot; &gt;&gt; $START_LOG
  exit 1
fi

# 애플리케이션 실행
echo &quot;[$NOW] &gt; $JAR 실행&quot; &gt;&gt; $START_LOG
nohup java -jar $JAR &gt; $APP_LOG 2&gt; $ERROR_LOG &amp;

# 실행된 프로세스의 PID 확인
NEW_SERVICE_PID=$(pgrep -f $JAR)
if [ ! -z &quot;$NEW_SERVICE_PID&quot; ]; then
  echo &quot;[$NOW] &gt; 새로운 서비스 PID: $NEW_SERVICE_PID&quot; &gt;&gt; $START_LOG
else
  echo &quot;[$NOW] &gt; 서비스 시작 실패&quot; &gt;&gt; $START_LOG
  exit 1
fi
</code></pre></br>

<h2 id="이번엔-진짜-배포-성공">이번엔 진짜 배포 성공</h2>
<p>스크립트 수정이 이뤄진 다음부터는 계속해서 배포에 성공한 것을 확인할 수 있었다.🥹 문제를 해결하게 되어 기쁘지만, 한편으로는 앞으로 프로덕션 환경에서 유사한 상황이 발생할때는 어떻게 대처하는게 현명한 방법일지 고민이 된다. 또 앞으로는 배포가 실패할 때 이에 대한 알림을 빠르게 받아볼 수 있게끔 환경을 구축해야겠다는 생각도 든다 💭
<img src="https://velog.velcdn.com/images/yul_ee/post/a014143a-8531-4ead-9a76-bf859b5caac2/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] 서버 용량 부족으로 인한 CodeDeploy 배포실패 해결하기😵‍💫]]></title>
            <link>https://velog.io/@yul_ee/AWS-CodeDeploy-%EB%B0%B0%ED%8F%AC%EC%8B%A4%ED%8C%A8-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yul_ee/AWS-CodeDeploy-%EB%B0%B0%ED%8F%AC%EC%8B%A4%ED%8C%A8-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 19 Sep 2024 16:13:26 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-발견">문제 발견</h2>
<p>&#39;프리비&#39; 서비스를 개발하는 과정에서 얼마전부터 계속 CodeDeploy를 이용한 배포가 실패하는 문제가 발생했다. 꽤 오래전부터 배포 실패가 계속되고 있었는데, 프론트엔드와 백엔드의 개발 속도가 다르다보니 로컬 테스트 위주로만 진행하고 배포환경에서 테스트를 소홀히해 문제 상황을 뒤늦게 인식하게 되었다. 😓</p>
</br>

<h2 id="문제-분석">문제 분석</h2>
<p>AWS CodeDeploy에 접속해 분석을 원하는 배포 ID를 클릭하면 해당 배포시도에 대한 세부 정보를 확인할 수 있고, 하단의 view events 버튼을 누르면 배포실패 원인에 대한 로그를 확인할 수 있다.
<img src="https://velog.velcdn.com/images/yul_ee/post/8f2e2ef4-cd57-4bb3-a89a-df5e749a35c4/image.png" alt=""></p>
</br>

<p>확인 결과 우리 서비스의 실패로그는 아래와 같았다. 배포를 시도했지만 ubuntu 서버 용량이 부족해 배포가 불가능했던 것이다.</p>
<pre style="background-color: #E7F2FB; padding: 10px;">
<code>
No space left on device @ fptr_finalize_flush - /opt/codedeploy-agent/deployment-root/ongoing-deployment/d-YH9263EJ7
</code>
</pre>
<p><img src="https://velog.velcdn.com/images/yul_ee/post/574a2955-7232-440e-87eb-25be99d975e4/image.png" alt=""></p>
</br>

<h2 id="서버-용량-확인">서버 용량 확인</h2>
<p>실패 로그에서 서버 용량 부족이 문제임을 알려주고 있으므로, 이를 실제로 확인해 볼 필요가 있었다. /dev/root 파일을 보면 할당된 7.6GB의 서버 용량을 전부 사용해 Use 100%인 것을 확인할 수 있었는데, 실제로 용량 부족이 원인이 되어 배포에 실패했음을 추측할 수 있다🫠 </p>
<p><img src="https://velog.velcdn.com/images/yul_ee/post/ce83f4ec-2ad8-4ac7-8051-c9269a41bedb/image.png" alt=""></p>
</br>

<p>문제가 되고 있는 /dev/root는 기본적으로 시스템 핵심 파일과 디렉토리들이 저장되는 공간으로, 실제 디렉토리는 아니고 루트 파티션을 참조하는 파일이다. 이 파일 시스템에는 다음과 같은 디렉토리들이 포함된다.</p>
<ul>
<li>/home: 사용자 데이터, 홈 디렉토리</li>
<li>/var: 로그 파일, 캐시 파일 등</li>
<li>/tmp: 임시 파일</li>
<li>/opt: 추가 애플리케이션</li>
<li>/usr: 시스템 프로그램, 라이브러리</li>
<li>/etc: 시스템 설정 파일</li>
</ul>
<p>즉, ubuntu 운영체제와 더불어 배포를 시도할때 설치되는 패키지, 로그, 배포파일 등 거의 모든 것이 저장되는 공간이다. 이 루트 파티션 전체의 용량이 부족한 것이기 때문에 용량 확보를 위해 불필요한 파일/디렉토리를 정리해주거나 아니면 서버 용량 자체를 늘려주는 방법을 생각할 수 있다.</p>
</br>

<h2 id="서버-용량-확보">서버 용량 확보</h2>
<p>우선 불필요한 디렉토리나 파일을 삭제해 서버 용량을 확보하고자 했다. 용량이 큰 디렉토리 순서로 정렬해보았다.
<img src="https://velog.velcdn.com/images/yul_ee/post/61f111b8-bebb-47ed-9731-b88908a4efca/image.png" alt=""></p>
<p>용량이 큰 디렉토리 순서로 탐색하면서 불필요한 파일을 삭제하고자 했지만, 지우면 안될 파일과 지워도 되는 파일을 구분하기가 힘들었고 무엇보다 많은 파일을 삭제했음에도 확보한 용량이 0.1GB에 불과했다 🤣 </p>
<p>sudo apt-get autoremove 명령어를 통해 불필요한 의존성 패키지를 제거해보기도 했지만 여전히 용량에 변동은 없었다..! 
<img src="https://velog.velcdn.com/images/yul_ee/post/fee922b6-d972-434f-8ed2-27157e12407f/image.png" alt=""></p>
</br>

<p>이렇게 하다가는 용량 확보하기가 너무 힘들 것 같아 아예 ec2 서버 용량을 늘리기로 했다. 현재는 기본 8GB 용량으로 설정되어 있는데, 우선 10GB로 조금만 늘려보기로 했다. 30GB까지가 프리티어로 지원되는데 현재 백엔드 서버 1개, 프론트엔드 서버 2개 각각에 8기가짜리 볼륨 3개가 이미 돌아가고 있기때문에 백엔드 서버 스토리지만 30으로 늘려버릴 수는 없었다. </p>
<ol>
<li><p>AWS EC2 -&gt; 좌측 탭의 볼륨 -&gt; 늘리려는 볼륨 선택  -&gt; 볼륨수정
<img src="https://velog.velcdn.com/images/yul_ee/post/5adca9f8-329c-4e5f-8b7e-67c69701b883/image.png" alt=""></p>
</li>
<li><p>볼륨 크기 수정
<img src="https://velog.velcdn.com/images/yul_ee/post/9fb9eee1-dd76-4576-928d-b74a2f448be8/image.png" alt=""></p>
</li>
<li><p>(2)단계에서 볼륨 크기를 수정하고 확인버튼을 누르면 아래와 같은 알림창이 뜬다. 여기서 <strong>볼륨 크기를 늘리는 경우 파일 시스템을 볼륨의 새 크기로 확장해야 합니다</strong> 이 문장이 중요하다! 단순히 볼륨 크기를 늘리면 물리적인 디스크 공간은 늘어나지만, 아직 파일시스템은 이 늘어난 공간을 인식하지 못하기 때문에 이를 인식할 수 있도록 별도로 설정해주는 작업이 필요하다.
<img src="https://velog.velcdn.com/images/yul_ee/post/f8b32e5c-8490-4a6d-be32-cf9ffa1aedfc/image.png" alt=""></p>
</li>
<li><p>파티션 확인
lsblk 명령어를 사용해 확장된 디바이스를 확인한다. 내 경우에는 nvme0n1 디스크의 nvme0n1p1 용량을 증가시켜야 한다.
<img src="https://velog.velcdn.com/images/yul_ee/post/1defe552-eb01-4ada-9a15-ca006dab4574/image.png" alt=""></p>
</li>
<li><p>파티션 크기 조정 &amp; 파일시스템 확장
파일시스템 <strong>확장 전</strong>에 /dev/root 파일시스템은 7.6GB 사이즈를 갖고있음을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/yul_ee/post/dbc86457-6ef0-48fa-bb3f-edf9a08fdd51/image.png" alt=""></p>
<pre style="background-color: #E7F2FB; padding: 10px;">
<code>
sudo growpart /dev/nvme0n1 1
sudo resize2fs /dev/root
</code>
</pre>
<p>위 명령어를 연속으로 입력해 파티션 크기를 조정하고 파일시스템 확장을 수행할 수 있다.</p>
</li>
<li><p>확장된 파일시스템 확인
10GB로 디스크 용량이 증가되었고 사용 가능한 공간 2.1GB가 확보되었다!
<img src="https://velog.velcdn.com/images/yul_ee/post/68a38629-626b-4e77-9610-50064c31c26d/image.png" alt=""></p>
</li>
</ol>
</br>

<h2 id="예방책">예방책</h2>
<p>이 외에도 예방책으로 서버에 과거 배포본을 저장해두는 최대 개수를 조정해두었다. Code deploy 에이전트는 디폴트로 최대 다섯 개의 배포본을 저장해두는데 프로젝트가 커질수록 이 배포본 하나 하나의 크기가 커지면서 서버 용량 부족 문제로 이어지는 경우가 많은 것 같았다. 
이 설정을 수정하려면 /etc/codedeploy-agent/conf 경로의 codedeployagent.yml 파일의 max_revisions를 조정하면 되는데, 나는 5 -&gt; 2로 수정해 최대 두 개의 배포본만 저장되게끔 했다.</p>
<p>설정을 변경할때 단순히 vi codedeployagent.yml로 편집기를 열어서 저장하려고 하면 쓰기 권한이 없어서 반영이 안되므로 sudo 키워드를 붙여야 한다! 그리고 이 설정을 반영하기 위해서는 아래 명령을 통해  codedeploy agent를 재실행해줘야 한다.</p>
<pre style="background-color: #E7F2FB; padding: 10px;">
<code>
sudo service codedeploy-agent restart
</code>
</pre>

</br>

<h2 id="배포-성공">배포 성공!</h2>
<p>은 아직 못했다 ㅜㅜ .. 용량을 늘리고 재배포를 시도했지만 또 새로운 오류가 발생했다🥲 다음 문제도 손보고 나면 진짜 배포 되겠지.. </p>
</br>

<hr>
<p>문제 해결에 참고한 글</p>
<ul>
<li><a href="https://prohannah.tistory.com/201">https://prohannah.tistory.com/201</a></li>
<li><a href="https://coldpresso.tistory.com/5">https://coldpresso.tistory.com/5</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[토비의 스프링 3.1] 8주차 스터디 - 6장 AOP(2)]]></title>
            <link>https://velog.io/@yul_ee/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-3.1-8%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-6%EC%9E%A5-AOP2</link>
            <guid>https://velog.io/@yul_ee/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-3.1-8%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-6%EC%9E%A5-AOP2</guid>
            <pubDate>Tue, 17 Sep 2024 13:43:17 GMT</pubDate>
            <description><![CDATA[<h1 id="66-트랜잭션-속성">6.6 트랜잭션 속성</h1>
<h2 id="661-트랜잭션-정의">6.6.1 트랜잭션 정의</h2>
<ul>
<li>트랜잭션이라고 모두 같은 방식으로 동작하는 것은 아님</li>
<li><code>commit</code>, <code>rollback</code> 외에도 트랜잭션의 동작 방식을 제어할 수 있는 몇 가지 조건이 있음</li>
</ul>
</br>

<h3 id="트랜잭션-전파">트랜잭션 전파</h3>
<blockquote>
<p>트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정</p>
</blockquote>
<p>A의 트랜잭션이 시작되고 아직 끝나지 않은 시점에서 B를 호출했다면, B의 코드는 어떤 트랜잭션 안에서 동작해야 할까?
<img width="351" alt="스크린샷 2024-09-16 19 46 44" src="https://github.com/user-attachments/assets/b9d05326-6731-464b-b102-020fc78292cc"></p>
<ol>
<li><p>B의 코드는 A에서 이미 시작한 트랜잭션에 참여한다. </p>
<p> → (2)의 코드를 진행하는 중에 예외가 발생하면 A, B코드에서 진행됐던 DB작업이 모두 취소됨</p>
</li>
<li><p>A와 무관하게 B를 독립적인 트랜잭션으로 만든다. </p>
<p> → (2)의 코드를 진행하는 중에 예외가 발생하더라도 B의 코드는 영향을 받지 않음</p>
</li>
</ol>
<ul>
<li><p><code>PROPAGATION_REQUIRED</code></p>
<ul>
<li><p>가장 많이 사용되는 트랜잭션 전파 속성</p>
</li>
<li><p>진행중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있다면 이에 참여한다.</p>
</li>
<li><p>해당 속성을 갖는 코드는 다양하게 결합해서 하나의 트랜잭션으로 구성하기 쉽다.</p>
<p>  ex) A, B, A→B, B→A</p>
</li>
</ul>
</li>
<li><p><code>PROPAGATION_REQUIRES_NEW</code></p>
<ul>
<li>항상 새로운 트랜잭션을 시작</li>
</ul>
</li>
<li><p><code>PROPAGATION_NOT_SUPORTED</code></p>
<ul>
<li>트랜잭션 없이 동작하도록 만들고, 진행중인 트랜잭션이 있어도 무시</li>
<li>이럴거면 트랜잭션 경계설정을 아예 안하면 되는거 아닌가?<ul>
<li>트랜잭션 경계설정은 보통 AOP를 이용해 한 번에 많은 메소드에 동시에 적용하는 방법을 사용하기 때문에 이런 경우에 <strong>특별한 메소드만 트랜잭션에서 제외</strong>하기 위해 설정</li>
<li>물론 포인트컷을 잘 만들어서 사용하면 되지만 상당히 복잡해질 수 있기 때문에 차라리 모든 메소드에 트랜잭션 AOP를 설정하고 특정 메소드에만 해당 속성을 사용하는게 낫다고 함</li>
</ul>
</li>
</ul>
</li>
</ul>
</br>

<h3 id="격리수준">격리수준</h3>
<ul>
<li>모든 DB 트랜잭션은 격리수준을 갖고 있어야 함</li>
<li>서버환경에서는 여러 개의 트랜잭션이 동시에 진행될 수 있음<ul>
<li>모든 트랜잭션이 순차적으로 진행되면 성능이 크게 떨어지므로, 격리수준을 조정해 가능한 많은 트랜잭션을 동시에 진행시키면서도 문제가 발생하지 않게끔 제어한다.</li>
</ul>
</li>
<li>기본적으로는 DB, DataSource에 설정된 디폴트 격리수준을 따르지만 필요에 따라 메소드별로 독자적인 격리수준을 지정하기도 한다.</li>
</ul>
</br>

<h3 id="제한시간">제한시간</h3>
<ul>
<li>트랜잭션을 수행하는 제한시간을 설정할 수 있음</li>
<li>트랜잭션을 직접 시작할 수 있는 <code>PROPAGATION_REQUIRED</code>, <code>PROPAGATION_REQUIRES_NEW</code> 속성과 같이 사용해야 의미가 있음</li>
</ul>
</br>

<h3 id="읽기전용">읽기전용</h3>
<ul>
<li>트랜잭션 내에서 데이터를 조작하는 시도를 막아줌</li>
</ul>
</br>

<h2 id="662-트랜잭션-인터셉터와-트랜잭션-속성">6.6.2 트랜잭션 인터셉터와 트랜잭션 속성</h2>
<p>💡 원하는 메소드만 선택해서 독자적인 트랜잭션 정의를 적용할 수 있는 방법이 없을까?</p>
<ul>
<li><p><code>TransactionDefinition</code>  네 가지 속성을 이용해 트랜잭션의 동작방식을 제어</p>
</li>
<li><p><code>TransactionAdvice</code> TransactionDefinition 오브젝트를 생성하고 사용, 트랜잭션 경계설정 기능을 가짐</p>
</li>
</ul>
</br>

<h3 id="transactioninterceptor">TransactionInterceptor</h3>
<ul>
<li><p>트랜잭션 경계설정 어드바이스로 사용 가능</p>
</li>
<li><p>트랜잭션 정의를 메소드 이름 패턴을 사용해 지정할 수 있는 방법을 제공해줌</p>
</li>
<li><p>TransactionInterceptor의 두 가지 프로퍼티</p>
<ol>
<li>PlatformTransactionManager</li>
<li>Properties - transactionAttributes<ul>
<li>TransactionDefinition의 네 가지 기본 트랜잭션 속성 + rollbackOn() 메소드를 갖는 TransactionAttribute 인터페이스</li>
<li>트랜잭션 부가기능의 동작방식을 모두 제어할 수 있음</li>
</ul>
</li>
</ol>
</li>
<li><p>TransactionInterceptor의 예외 처리 방식</p>
<ol>
<li><p><code>런타임 예외</code>가 발생하면 트랜잭션을 롤백시킨다.</p>
</li>
<li><p>타깃 메소드가 런타임 예외가 아닌 <code>체크 예외</code>를 던지는 경우에는 트랜잭션을 커밋한다.</p>
<p> → 체크 예외를 일종의 비즈니스 로직에 따른 의미 있는 리턴 방식의 한 가지로 인식</p>
</li>
</ol>
</li>
<li><p>TransactionAttribute는 <code>rollbackOn()</code> 속성을 둬 이런 기본적인 예외 처리 방식을 따르지 않아도 되게끔 해준다.</p>
</li>
</ul>
</br>

<h3 id="트랜잭션-속성-정의-방식">트랜잭션 속성 정의 방식</h3>
<ol>
<li>TransactionInterceptor 타입의 빈 정의</li>
<li>tx 스키마의 전용 태그를 이용해 정의</li>
</ol>
</br>

<h2 id="663-포인트컷과-트랜잭션-속성의-적용-전략">6.6.3 포인트컷과 트랜잭션 속성의 적용 전략</h2>
<h3 id="트랜잭션-포인트컷-표현식은-타입-패턴이나-빈-이름을-이용한다">트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다</h3>
<ul>
<li>일반적으로 트랜잭션을 적용할 타깃 클래스의 메소드는 모두 트랜잭션 적용 후보가 되는 것이 바람직하다.<ul>
<li>비즈니스 로직을 담고 있는 클래스라면 메소드 단위까지 세밀하게 포인트컷을 정의해줄 필요는 없음</li>
</ul>
</li>
<li>쓰기 작업이 없는 단순한 조회 작업 메소드에도 모두 트랜잭션을 적용하는게 좋다.</li>
<li>트랜잭션 경계로 삼을 클래스들이 모여 있는 패키지 혹은 비즈니스 로직 서비스를 담당하는 클래스명의 패턴을 찾아서 표현식으로 만든다.<ul>
<li>메소드 시그니처를 이용한 execution() 방식의 포인트컷 표현식<ul>
<li>Service, ServiceImpl로 끝나는 경우 execution(<em><em>..</em>ServiceImpl.</em>(..))</li>
</ul>
</li>
<li>스프링의 빈 이름을 이용하는 bean() 표현식<ul>
<li>bean(*Service)</li>
</ul>
</li>
</ul>
</li>
</ul>
</br>

<h3 id="공통된-메소드-이름-규칙을-통해-최소한의-트랜잭션-어드바이스와-속성을-정의한다">공통된 메소드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의한다</h3>
<ul>
<li><p>기준이 되는 몇 가지 트랜잭션 속성을 정의하고, 그에 따라 적절한 메소드 명명 규칙을 만들어두면 하나의 어드바이스만으로 애플리케이션 모든 서비스 빈에 트랜잭션 속성을 지정할 수 있다.</p>
<ul>
<li><p>get으로 시작하는 메소드에 대해서는 읽기 전용 속성을 부여할 수 있음</p>
<pre><code class="language-xml">  &lt;tx:advice id=&quot;transactionAdvice&quot;&gt;
      &lt;tx:attributes&gt;
          &lt;tx:method name=&quot;get*&quot; read-only=&quot;true&quot; /&gt;
          &lt;tx:method name=&quot;*&quot; /&gt;
      &lt;/tx:attributes&gt;
  &lt;/tx:attributes&gt;</code></pre>
</li>
</ul>
</li>
</ul>
</br>

<h3 id="프록시-방식-aop는-같은-타킷-오브젝트-내의-메소드를-호출할-때는-적용되지-않는다">프록시 방식 AOP는 같은 타킷 오브젝트 내의 메소드를 호출할 때는 적용되지 않는다</h3>
<ul>
<li><p>프록시 방식의 AOP에서는 프록시를 통한 부가기능의 적용은 <strong>클라이언트</strong>로부터 호출이 일어날 때만 가능하다.
_<img width="569" alt="스크린샷 2024-09-16 20 57 49" src="https://github.com/user-attachments/assets/d0699b68-5a9c-4537-8645-4e6f61bc8ced">_</p>
<ul>
<li><p>(2)를 통해 update() 메소드가 호출될 때, 전파속성이 <code>REQUIRES_NEW</code>로 설정되어 있더라도 프록시의 delete()메소드에서 시작된 트랜잭션에 참여하게 된다.</p>
<ul>
<li>즉 타깃 오브젝트 안에서 메소드 호출이 일어나는 경우에는 부가기능 적용이 되지 않음</li>
</ul>
</li>
<li><p>타깃 안에서의 호출에 프록시가 적용되지 않는 문제를 해결할 수 있는 방법</p>
<ul>
<li>AspectJ와 같은 타깃의 바이트코드를 직접 조작하는 방식의 AOP 기술 적용</li>
</ul>
</li>
</ul>
</li>
</ul>
</br>

<h3 id="트랜잭션-속성-테스트">트랜잭션 속성 테스트</h3>
<img width="559" alt="스크린샷 2024-09-16 21 04 40" src="https://github.com/user-attachments/assets/3d449154-9151-4f94-b07b-6226db5936f0">

<img width="560" alt="image" src="https://github.com/user-attachments/assets/67920aa7-89cd-4091-a92d-e9035c169fe3">


<p>  → <code>TransientDataAccessResourceException</code> 예외가 발생한다. 스프링의 DataAccessException의 한 종류로 일시적인 예외상황을 만났을 때 발생하는 예외다.</p>
</br>

<hr>
<h1 id="67-애노테이션-트랜잭션-속성과-포인트컷">6.7 애노테이션 트랜잭션 속성과 포인트컷</h1>
<p>트랜잭션 속성을 비즈니스 로직을 담당하는 패키지, 클래스 등에 일괄적으로 적용하는 방식은 대부분의 상황에 잘 들어맞는다. 하지만 세밀한 제어가 필요한 경우도 종종 있는데, 이때는 직접 타깃에 트랜잭션 속성정보를 가진 애노테이션을 지정한다.</p>
<h2 id="671-트랜잭션-애노테이션">6.7.1 트랜잭션 애노테이션</h2>
<h3 id="transactional">@Transactional</h3>
<pre><code class="language-java">//Transactional.class

@Inherited
@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Transactional {
    TxType value() default Transactional.TxType.REQUIRED;

    @Nonbinding
    Class[] rollbackOn() default {};

    @Nonbinding
    Class[] dontRollbackOn() default {};

    public static enum TxType {
        REQUIRED,
        REQUIRES_NEW,
        MANDATORY,
        SUPPORTS,
        NOT_SUPPORTED,
        NEVER;

        private TxType() {
        }
    }
}</code></pre>
<ul>
<li>@Target<ul>
<li>메소드와 타입이 타깃이므로 메소드, 클래스, 인터페이스에 @Transactional 애노테이션을 사용할 수 있음</li>
</ul>
</li>
<li>스프링은 @Transactional이 부여된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식한다.<ul>
<li>이때 <code>TransactionAttributeSorcePointcut</code> 포인트컷을 사용하는데 표현식과 같은 선정기준을 갖는 것이 아니라, 해당 애노테이션이 붙은 빈 오브젝트를 모두 찾아서 포인트컷의 선정 결과로 돌려준다.</li>
</ul>
</li>
</ul>
</br>

<h3 id="트랜잭션-속성을-이용하는-포인트컷">트랜잭션 속성을 이용하는 포인트컷</h3>
<p>@Transactional 애노테이션을 사용했을 때 어드바이저의 동작방식
<img width="567" alt="스크린샷 2024-09-16 21 13 52" src="https://github.com/user-attachments/assets/9fcfdbf1-4d6a-4377-9418-03acafd4e643"></p>
<ul>
<li>포인트컷과 트랜잭션 속성을 애노테이션 하나로 지정 가능</li>
<li>메소드 단위로 지정할 수 있으므로 세밀한 제어가 가능</li>
<li>애노테이션이 메소드마다 반복적으로 부여되어 코드가 지저분해질 수 있음</li>
</ul>
</br>

<h3 id="대체-정책">대체 정책</h3>
<p>스프링은 @Transactional을 적용할 때 4단계의 대체정책을 이용한다.</p>
<ul>
<li><p>메소드의 속성을 확인할 때 다음의 순서로 트랜잭션이 적용됐는지 차례로 확인하고, 가장 먼저 발견되는 속성을 사용하게 하는 방법</p>
<ul>
<li>타깃 메소드 → 타깃 클래스 → 선언 메소드 → 선언 타입</li>
</ul>
</li>
<li><p>예시) @Transactional을 부여할 수 있는 위치는 총 6개</p>
</li>
</ul>
<img width="579" alt="스크린샷 2024-09-16 21 18 34" src="https://github.com/user-attachments/assets/1c37c029-b013-4677-b384-dedc1eac645e">

<ul>
<li>스프링은 5 → 6 → 4 → 2 → 3 → 1의 순서로 애노테이션을 찾는다.</li>
</ul>
<ul>
<li>대체정책을 잘 활용하면 애노테이션을 최소한으로 사용하면서 세밀하게 제어할 수 있음<ol>
<li>타입 레벨에 정의할 수 있는지 본다.</li>
<li>공통 속성을 따르지 않는 메소드에 대해서만 메소드 레벨에 애노테이션을 부여한다.</li>
</ol>
</li>
</ul>
</br>

<h2 id="672-트랜잭션-애노테이션-적용">6.7.2 트랜잭션 애노테이션 적용</h2>
<p>애노테이션을 이용할 때는 단순하게 트랜잭션이 필요한 타입 또는 메소드에 직접 애노테이션을 부여한다. 
<img width="578" alt="스크린샷 2024-09-16 21 23 44" src="https://github.com/user-attachments/assets/a2abda10-8d7a-4026-9615-7dc84600af3c"></p>
</br>

<hr>
<h1 id="68-트랜잭션-지원-테스트">6.8 트랜잭션 지원 테스트</h1>
<h2 id="681-선언적-트랜잭션과-트랜잭션-전파-속성">6.8.1 선언적 트랜잭션과 트랜잭션 전파 속성</h2>
<ul>
<li><p>사용자 등록 로직을 담당하는 add() 메소드가 트랜잭션 전파 방식을 사용할 수 없어서 매번 새로운 트랜잭션을 시작하도록 만들어졌다면, 다른 메소드에서 호출하기가 꺼려질 것이다.</p>
<ul>
<li>결국 add() 메소드를 복사해서 하나의 메소드 안에 넣게 되는 중복코드가 계속 발생할 것이다.</li>
</ul>
</li>
<li><p>스프링의 <code>선언적 트랜잭션</code>을 사용하면 AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있다</p>
</li>
</ul>
<img width="572" alt="스크린샷 2024-09-16 21 26 32" src="https://github.com/user-attachments/assets/c190d854-2142-488a-92b2-5428f84a03e9">


<ul>
<li>또한 스프링은 개별 데이터 기술의 트랜잭션 API를 사용해 직접 코드 안에서 사용하는 <code>프로그램에 의한 트랜잭션</code>도 지원한다.</li>
</ul>
</br>

<h2 id="682-트랜잭션-동기화와-테스트">6.8.2 트랜잭션 동기화와 테스트</h2>
<h3 id="트랜잭션-매니저와-트랜잭션-동기화">트랜잭션 매니저와 트랜잭션 동기화</h3>
<ul>
<li>트랜잭션 추상화 기술의 핵심은 트랜잭션 매니저와 트랜잭션 동기화<ul>
<li><code>트랜잭션 매니저</code> 구체적인 트랜잭션 기술의 종류에 상관없이 일관적인 제어 가능</li>
<li><code>트랜잭션 동기화</code> 시작된 트랜잭션 정보를 저장소에 보관해뒀다가 DAO에서 공유<ul>
<li>이 기술 덕분에 트랜잭션 전파 속성에 따라 진행 중인 트랜잭션이 있는지 확인하고 이에 참여할 수 있는 것</li>
</ul>
</li>
</ul>
</li>
</ul>
</br>

<h3 id="트랜잭션-매니저를-이용한-테스트용-트랜잭션-제어">트랜잭션 매니저를 이용한 테스트용 트랜잭션 제어</h3>
<img width="583" alt="스크린샷 2024-09-16 21 36 04" src="https://github.com/user-attachments/assets/4e570522-8421-4fcc-944d-c142754b408d">


<ul>
<li>메소드를 추가하지 않고도, UserService의 메소드를 호출하기 전에 트랜잭션을 미리 시작시켜서 UserService의 세 개의 메소드가 동일한 트랜잭션 내에 참여하게끔 유도할 수 있음</li>
</ul>
</br>

<h3 id="트랜잭션-동기화-검증">트랜잭션 동기화 검증</h3>
<img width="576" alt="스크린샷 2024-09-16 21 37 14" src="https://github.com/user-attachments/assets/d97d5fba-91d6-4b7f-b94e-1585f6ec8060">

<ul>
<li>테스트에서 미리 트랜잭션을 시작함으로써 테스트코드를 하나의 트랜잭션 단위로 묶는 것이 가능 (@Transactional 애노테이션 적용과 같은 용도로..)<ul>
<li>DB 작업이 포함되는 테스트를 원하는 대로 제어하는 것이 가능해졌다.</li>
<li>하이버네이트 등 ORM에서 세션에서 분리된 엔티티 동작을 확인할 때도 유용하다.</li>
</ul>
</li>
</ul>
</br>

<h3 id="롤백-테스트">롤백 테스트</h3>
<img width="578" alt="스크린샷 2024-09-16 21 40 24" src="https://github.com/user-attachments/assets/2d55f341-b947-4bbb-88ca-8fdf3a5df1e4">

<ul>
<li>DB에 엑세스하는 테스트를 할 때마다 테스트 데이터를 초기화하는 작업이 반복되는데, 이럴 때 롤백 테스트가 유용하다.</li>
<li>테스트에서 트랜잭션을 제어할 수 있기 때문에 얻을 수 있는 가장 큰 유익이 있다면 롤백 테스트다.<ul>
<li>MySQL에서는 동일한 작업을 수행한 뒤에 롤백하는게 커밋보다 더 빠르기 때문에 성능이 향상되기도 함</li>
</ul>
</li>
</ul>
</br>

<h2 id="683-테스트를-위한-트랜잭션-애노테이션">6.8.3 테스트를 위한 트랜잭션 애노테이션</h2>
<h3 id="transactional-1">@Transactional</h3>
<ul>
<li>테스트에서 사용하는 이 애노테이션은 AOP를 위한 것은 아님</li>
</ul>
</br>

<h3 id="rollback">@Rollback</h3>
<ul>
<li>테스트용 @Transactional 애노테이션은 테스트가 끝나면 자동으로 롤백된다.</li>
<li>강제 롤백을 원하지 않는 경우에는 @Rollback(false) 애노테이션을 사용하자</li>
</ul>
</br>

<h3 id="transactionconfiguration">@TransactionConfiguration</h3>
<ul>
<li><p>@Transactional은 테스트 클래스에 적용가능하지만, @Rollback은 메소드 레벨에만 적용 가능하다.</p>
</li>
<li><p>테스트 클래스의 모든 메소드에 트랜잭션을 적용하되 모든 트랜잭션을 롤백시키지 않고 커밋하려면 이 애노테이션을 사용해보자.</p>
<pre><code class="language-java">  @TransactionConfiguration(defaultRollback=false)</code></pre>
</li>
</ul>
</br>

<h3 id="nottransactional과-propagationnever">NotTransactional과 Propagation.NEVER</h3>
<ul>
<li><p>필요하지도 않은 트랜잭션이 만들어지는 것이 싫은 경우에 적용한다.</p>
<pre><code class="language-java">  @Transactional(propagation=Propagation.NEVER)</code></pre>
</li>
</ul>
</br>

<h3 id="효과적인-db-테스트">효과적인 DB 테스트</h3>
<ul>
<li>일반적으로 의존, 협력 오브젝트를 사용하지 않는 단위 테스트와, DB 같은 외부의 리소스나 여러 계층 클래스가 참여하는 통합 테스트는 아예 구분해서 만드는 게 좋다.</li>
<li>테스트는 어떤 경우에도 서로 의존하면 안 된다.</li>
</ul>
</br>

<hr>
<h1 id="69-정리">6.9 정리</h1>
<ul>
<li>목 오브젝트를 활용하면 의존관계에 있는 오브젝트로 쉽게 고립된 테스트로 만들 수 있다.</li>
<li>DI를 이용한 트랜잭션의 분리는 데코레이터 패턴과 프록시 패턴으로 이해될 수 있다.</li>
<li>AOP는 OOP만으로 모듈화하기 힘든 부가기능을 효과적으로 모듈화하도록 도와주는 기술이다.</li>
<li>AOP를 이용해 트랜잭션 속성을 지정할 때, 포인트컷 표현식과 메소드 이름 패턴을 이용하는 방법과 타깃에 직접 부여하는 @Transactioanl 애노테이션을 사용하는 방법이 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[토비의 스프링 3.1] 7주차 스터디 - 6장 AOP(1)]]></title>
            <link>https://velog.io/@yul_ee/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-3.1-7%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@yul_ee/%ED%86%A0%EB%B9%84%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-3.1-7%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Mon, 16 Sep 2024 11:14:40 GMT</pubDate>
            <description><![CDATA[<hr>
<p>6장 AOP</p>
<ul>
<li>6.3 다이내믹 프록시와 팩토리 빈</li>
<li>6.4 스프링의 프록시 팩토리 빈</li>
<li>6.5 스프링 AOP</li>
</ul>
<hr>
</br>

<h1 id="63-다이내믹-프록시와-팩토리-빈">6.3 다이내믹 프록시와 팩토리 빈</h1>
<h2 id="631-프록시와-프록시-패턴-데코레이터-패턴">6.3.1 프록시와 프록시 패턴, 데코레이터 패턴</h2>
<p><img src="https://github.com/user-attachments/assets/d065de45-6724-429b-9881-a0c928ecd8af" alt="image"></p>
<ul>
<li>부가기능 외의 나머지 모든 기능은 원래 핵심기능을 가진 클래스로 위임해줘야 한다. 핵심기능은 부가기능 클래스의 존재를 모르며, 부가기능이 핵심기능을 사용하는 구조가 된다.</li>
<li>클라이언트가 핵심기능을 가진 클래스를 직접 사용해버리면 부가기능이 적용될 기회가 없다는 문제가 존재한다.</li>
</ul>
<p><img src="https://github.com/user-attachments/assets/6dd1e8a3-322a-4e5d-9c56-ac97195b6299" alt="image 1"></p>
<ul>
<li>부가기능은 자신이 핵심기능을 가진 클래스처럼 꾸며서, 클라이언트가 자신을 거쳐 핵심기능을 사용하도록 만든다.</li>
<li>클라이언트는 인터페이스를 통해서만 핵심기능을 사용하게 하고, 부가기능도 같은 인터페이스를 구현한 뒤에 자신이 그 사이에 끼어들어야 한다.</li>
</ul>
</br>

<h3 id="프록시">프록시</h3>
<p><img src="https://github.com/user-attachments/assets/fd51d5a9-0f8b-47bb-9106-918ab4a7ea8e" alt="image 2"></p>
<p>자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리자, 대리인과 같은 역할을 한다고 해서 <code>프록시</code>라고 부른다. 프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트를 <code>타깃</code> 또는 <code>실체</code>라고 부른다.</p>
<ul>
<li>프록시의 특징<ul>
<li>타깃과 같은 인터페이스를 구현한다.</li>
</ul>
</li>
</ul>
<ul>
<li><p>프록시의 사용 목적</p>
<ol>
<li><p><strong>클라이언트가 타깃에 접근하는 방법을 제어하기 위함</strong></p>
</li>
<li><p><strong>타깃에 부가적인 기능을 제공해주기 위함</strong></p>
<p>→ 이 사용 목적에 따라 두 개는 다른 디자인패턴으로 구분된다.</p>
</li>
</ol>
</li>
</ul>
</br>

<h3 id="데코레이터-패턴">데코레이터 패턴</h3>
<blockquote>
<p>타깃에 부가적인 기능을 <strong>런타임</strong> 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴 (2에 해당)</p>
</blockquote>
<ul>
<li>부가적인 기능을 다이내믹하게 부여<pre><code>= 컴파일 시점에 (즉, 코드 상에서) 어떤 방법과 순서로 프록시-타깃이 연결되어 사용되는지 정해져있지 않다.</code></pre></li>
</ul>
<ul>
<li>데코레이터
  = 제품이나 케이크를 여러 겹으로 포장하고 그 위에 장식을 붙이는 것처럼 실제 내용물은 동일하지만 부가적인 효과를 부여해줄 수 있다는 의미<ul>
<li>프록시는 한 개로 제한되지 않으며, 프록시가 직접 타깃을 사용하도록 고정시킬 필요도 없다.</li>
</ul>
</li>
</ul>
<ul>
<li><p>데코레이터 패턴 적용 예
<img src="https://github.com/user-attachments/assets/d19e8e21-30ee-4c91-a6d6-9c4f4bc6b7f2" alt="image 3"></p>
<ul>
<li>프록시가 여러 개인 만큼 순서를 정해서 단계적으로 위임하는 구조를 만든다.</li>
<li>부가적인 기능을 프록시로 만들어두고 런타임 시에 이를 적절한 순서로 조합해서 사용한다.</li>
</ul>
</li>
</ul>
<ul>
<li><p>프록시로 동작하는 각 데코레이터는 위임하는 대상에도 인터페이스로 접근하기 때문에 자신이 최종 타깃으로 위임하는지, 아니면 다음 단계의 데코레이터 프록시로 위임하는지 알지 못한다.</p>
<p>  → 데코레이터의 다음 위임 대상은 인터페이스로 선언하고 생성자나 수정자 메소드를 통해 위임 대상을 외부에서 런타임 시에 주입받을 수 있도록 만들어야 한다.</p>
</li>
</ul>
<ul>
<li><p>데코레이터 패턴이 적용된 대표적인 예시</p>
<pre><code class="language-java">  InputStream is = new BufferdInputStream(new FileInputStream(&quot;a.txt&quot;));</code></pre>
<ul>
<li>Component 인터페이스: <code>InputStream</code>이 공통 인터페이스 역할을 한다.</li>
<li>Concrete Component &amp; 타깃: <code>FileInputStream</code>은 기본 기능을 제공하는 구체적인 구현이다.</li>
<li>Decorator: <code>BufferedInputStream</code>은 FileInputStream을 감싸서 버퍼링 기능을 추가해 read() 메서드가 효율적으로 동작할 수 있도록 돕는다.</li>
</ul>
</li>
<li><p>UserService 인터페이스를 구현한 타깃인 UserServiceImpl에 트랜잭션 부가기능을 제공해주는 UserServiceTx를 추가한 것도 데코레이터 패턴의 적용이라고 볼 수 있다.</p>
<pre><code class="language-java">  &lt;bean id=&quot;userService&quot; class=&quot;springbook.user.service.UserServiceTx&quot;&gt;  
          &lt;property name=&quot;transactionManager&quot; ref=&quot;transactionManager&quot; /&gt;
          **&lt;property name=&quot;userService&quot; ref=&quot;userServiceImpl&quot; /&gt;**  
      &lt;/bean&gt;

  &lt;bean id=&quot;**userServiceImpl**&quot; class=&quot;springbook.user.service.UserServiceImpl&quot;&gt;</code></pre>
<p>  → UserServiceImpl로 선언된 타깃 빈이 DI를 통해 데코레이터인 UserService빈에 주입되도록 설정</p>
</li>
<li><p>언제 유용할까?</p>
  <aside>

<p>  타깃의 코드를 손대지 않고, 클라이언트가 호출하는 방법도 변경하지 않은 채로 새로운 기능을 추가할 때</p>
  </aside>


</li>
</ul>
</br>

<h3 id="프록시-패턴">프록시 패턴</h3>
<blockquote>
<p>클라이언트가 타깃에 접근하는 방법을 제어하려는 목적을 가진다. (1에 해당)</p>
</blockquote>
<ul>
<li>타깃의 기능을 확장/추가해주진 않지만 클라이언트가 타깃에 접근하는 방식을 변경해준다.<ul>
<li>타깃 오브젝트가 필요해지는 시점까지 오브젝트를 생성하지 않는 편이 좋지만 타깃 오브젝트에 대한 레퍼런스가 미리 필요할 수 있는데, 이때 프록시 패턴을 적용하면 좋다.</li>
<li>클라이언트에게 타깃에 대한 레퍼런스를 넘겨야 할 때 실제 오브젝트 대신 프록시를 넘겨준다. 그리고 프록시의 메소드를 사용해 타깃을 사용하려고 시도하면 그때 프록시가 타깃 오브젝트를 생성하고 요청을 위임해준다.
→ 프록시를 통해 타깃의 생성을 최대한 늦춤</li>
</ul>
</li>
</ul>
<ul>
<li><p>특별한 상황에서 타깃에 대한 접근권한을 제어</p>
<ul>
<li><p>수정 가능한 오브젝트가 특정 레이어에서는 읽기 전용으로 사용되어야 할 때, 오브젝트의 프록시를 만들 수 있다. 프록시의 특정 메소드를 사용하려고 하면 접근이 불가능하다고 예외를 발생시킨다.</p>
<ul>
<li><p>Collections의 unmodifiableCollection()을 통해 만들어지는 오브젝트가 전형적인 접근권한 제어용 프록시</p>
</li>
<li><p>파라미터로 전달된 Collection의 프록시를 만들어서 add(), remove() 등 수정 메소드를 호출하면 예외를 발생하게 해준다.</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>프록시 패턴의 경우 자신이 만들거나 접근할 타깃 클래스의 정보를 알고 있는 경우가 많다.</p>
</li>
<li><p>프록시 패턴 + 데코레이터 패턴
<img src="https://github.com/user-attachments/assets/30d21140-b1a6-423b-aef0-733969cec07f" alt="image 4"></p>
</li>
</ul>
</br>

<h2 id="632-다이내믹-프록시">6.3.2 다이내믹 프록시</h2>
<p>프록시를 일일이 모든 인터페이스를 구현해서 클래스를 새로 정의하지 않고도 편리하게 만들어 사용할 방법이 있다. 몇 가지 API를 이용해 프록시처럼 동작하는 오브젝트를 다이내믹하게 생성하는 것이다.</p>
<h3 id="프록시의-구성과-프록시-작성의-문제점">프록시의 구성과 프록시 작성의 문제점</h3>
<pre><code class="language-java">public class UserServiceTx implements UserService {
    UserService userService;

    // 프록시의 기능(1). 메소드 구현과 위임
    public void add(User user) {
        this.userService.add(user);
    }

    public void upgradeLevels() {
        // 프록시의 기능(2). 부가기능 수행
        TransactionStatus status = this.transactionManager
                .getTransaction(new DefaultTransactionDefinition());
        try {

            userService.upgradeLevels(); // (1). 메소드 구현과 위임

            this.transactionManager.commit(status);
        } catch (RuntimeException e) {
            this.transactionManager.rollback(status);
            throw e;
        }
    }
}</code></pre>
<p>프록시를 만들기 번거로운 이유</p>
<ol>
<li><p><strong>부가기능 코드가 중복될 가능성이 많다.</strong> </p>
<p> → 중복되는 코드를 분리해서 어떻게든 해결할 수 있다.</p>
</li>
<li><p><strong>인터페이스를 구현하고 위임하는 코드를 작성하기 번거롭다.</strong> 부가기능이 필요없는 메소드도 구현해서 타깃으로 위임하는 코드를 일일이 만들어줘야 한다.</p>
<p> → <code>JDK의 다이내믹 프록시</code>를 이용해 해결할 수 있다.</p>
</li>
</ol>
</br>

<h3 id="리플렉션">리플렉션</h3>
<blockquote>
<p>다이내믹 프록시는 <strong>리플렉션</strong> 기능을 이용해서 프록시를 만들어준다. 리플렉션은 자바의 코드 자체를 추상화해서 접근하도록 만든 것을 말한다.</p>
</blockquote>
<ul>
<li><p>자바의 모든 클래스는 그 클래스 자체의 구성정보를 담은 <code>Class</code> 타입의 오브젝트를 하나 갖는다.</p>
<ul>
<li>클래스 코드에 대한 메타정보를 가져오거나 오브젝트를 조작할 수 있다.<ul>
<li>클래스 이름, 어떤 클래스를 상속하는지, 어떤 인터페이스를 구현했는지, 어떤 필드를 갖고 있고, 각각의 타입은 무엇인지, 메소드는 어떤 것을 정의했고, 메소드의 파라미터와 리턴 값은 무엇인지 등</li>
</ul>
</li>
</ul>
</li>
<li><p>리플렉션 API 중, 메소드에 대한 정의를 담은 <code>Method</code> 인터페이스를 이용해 메소드 호출하는 방법</p>
<pre><code class="language-java">  //Method를 이용해 리플렉션 방식으로 호출
  Method lengthMethod = String.class.getMethod(&quot;length&quot;);</code></pre>
<pre><code class="language-java">  // 특정 오브젝트의 메소드를 실행시킬 수도 있다.
  public Object invoke(Obeject obj, Object ...args)

  int length = lengthMethod.invoke(name); // int length = name.length();</code></pre>
</li>
</ul>
</br>

<h3 id="프록시-클래스">프록시 클래스</h3>
<ol>
<li><p>Hello 인터페이스</p>
<pre><code class="language-java"> interface Hello { 
     String sayHello(String name);
     String sayHi(String name);
     String sayThankYou(String name);
 }</code></pre>
</li>
<li><p>타깃 클래스</p>
<pre><code class="language-java"> public class HelloTarget implements Hello { 
     public String sayHello(String name) { return &quot;Hello &quot; + name;}
     public String sayHi(String name) { return &quot;Hi &quot; + name;}
     public String sayThankYou(String name) { return &quot;Thank You &quot; + name;
     } 
 }</code></pre>
</li>
<li><p>프록시 클래스</p>
<ul>
<li><p>데코레이터 패턴을 적용해 부가기능을 추가한다.</p>
<pre><code class="language-java">public class HelloUppercase implements Hello { 
  Hello hello;

  public HelloUppercase(Hello hello) { 
      this.hello = hello;
  }

  public String sayHello(String name) { 
      return hello.sayHello(name).toUpperCase(); // 위임과 부가기능 적용
  }

  public String sayHi(String name) { 
      return hello.sayHi(name).toUpperCase();
  }

  public String sayThankYou(String name) { 
      return hello.sayThankYou(name).toUpperCase();
  }

}</code></pre>
<p>→ 인터페이스의 모든 메소드를 구현해 위임하도록 만들어야 하고, 부가기능인 리턴값을 대문자로 바꿔주는 기능이 모든 메소드에 중복된다는 <strong>프록시 적용의 일반적인 문제점 두가지</strong>를 모두 갖는다.</p>
</li>
</ul>
</li>
</ol>
</br>

<h3 id="다이내믹-프록시-적용">다이내믹 프록시 적용</h3>
<p><img src="https://github.com/user-attachments/assets/5fc643b6-5a45-42cc-9da7-9b8fc73392b8" alt="image 5"></p>
<ul>
<li><p>다이내믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어진다. 클라이언트는 다이내믹 프록시 오브젝트를 타깃 인터페이스를 통해 사용할 수 있다.</p>
<ul>
<li><strong>인터페이스를 구현하고 위임하는 코드를 작성하기 번거롭다</strong>는 문제 해결</li>
<li><code>프록시 팩토리</code>에게 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 클래스의 오브젝트를 자동으로 만들어주기 때문</li>
</ul>
</li>
<li><p><code>다이내믹 프록시</code>가 인터페이스 구현 클래스의 오브젝트를 만들어주지만, 부가기능 제공 코드는 직접 작성해야 한다.</p>
<ul>
<li>부가기능은 프록시 오브젝트와 독립적으로 InvocationHandler를 구현한 오브젝트에 담는다.</li>
</ul>
</li>
<li><p><code>InvocationHandler</code> 인터페이스는 다음의 메소드 하나만을 갖는다.</p>
<pre><code class="language-java">  public Object invoke(Object proxy, Method method, Object[] args) </code></pre>
<p>  → 다이내믹 프록시 오브젝트는 클라이언트의 모든 요청을 리플렉션 정보로 변환해서 InvocationHandler 구현 오브젝트의 invoke() 메소드로 넘기는 것.</p>
</li>
<li><p>각 메소드 요청은 어떻게 처리할까?</p>
  <aside>

<p>  InvocationHandler 구현 오브젝트가 타깃 오브젝트 레퍼런스를 갖고 있다면 리플렉션을 이용해 간단한 위임 코드를 만들어낼 수 있다.</p>
  </aside>


</li>
</ul>
<p><img src="https://github.com/user-attachments/assets/058eced6-3933-4aaf-83b0-e74394ed1d3e" alt="image 6"></p>
<pre><code class="language-java">    public class UppercaseHandler implements InvocationHandler {
        Hello target; // 타깃의 정보를 갖고 있다.

        public UppercaseHandler(Hello target) {
            this.target = target;
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String ret = (String)method.invoke(target, args); //타깃으로 위임
            return ret.toUpperCase(); // 부가기능 제공
        }
    }</code></pre>
<pre><code class="language-java">    // 프록시 생성
    Hello proxiedHello = (Hello)Proxy.newProxyInstance(
            getClass().getClassLoader(),
            new Class[] { Hello.class }, //구현할 인터페이스
            new UpperCaseHandler(new HelloTarget())); // 부가기능과 위임코드를 담은 InvocationHandler</code></pre>
</br>

<h3 id="다이내믹-프록시의-확장">다이내믹 프록시의 확장</h3>
<p>다이내믹 프록시를 사용하면 직접 정의해서 만든 프록시보다 훨씬 유연하고 많은 장점이 있다. </p>
<ol>
<li><p>Hello 인터페이스의 메소드가 늘어나도 일일이 메소드를 구현할 필요가 없다.</p>
<p> → 다이내믹 프록시가 생겨날때 추가된 메소드가 자동으로 포함되기 때문</p>
</li>
<li><p>InvocationHandler는 리플렉션의 Method 인터페이스를 이용해 타깃의 메소드를 호출하므로, 타깃의 종류에 상관없이 적용이 가능하다. </p>
</li>
</ol>
</br>


<h2 id="634-다이내믹-프록시를-위한-팩토리-빈">6.3.4 다이내믹 프록시를 위한 팩토리 빈</h2>
<ul>
<li>스프링은 지정된 클래스 이름을 가지고 리플렉션을 이용해 해당 클래스의 오브젝트를 만든다.</li>
</ul>
<pre><code class="language-java">    Date now = (Date) Class.forName(&quot;java.util.Date&quot;).newInstance();</code></pre>
<ul>
<li>다이내믹 프록시 오브젝트는 클래스가 어떤 것인지 알 수 없다. 클래스 자체도 내부적으로 다이내믹하게 정의해서 사용하기 때문에 사전에 클래스 정보를 알아내서 스프링 빈에 정의할 방법이 없다.</li>
</ul>
</br>

<h3 id="팩토리-빈">팩토리 빈</h3>
<blockquote>
<p>스프링을 대신해 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈을 말한다.</p>
</blockquote>
<ul>
<li><code>FactoryBean</code> 인터페이스를 구현해 팩토리 빈을 생성할 수 있다.</li>
<li>스프링은 FactoryBean 인터페이스를 구현한 클래스가 빈의 클래스로 지정되면, 팩토리 빈 클래스의 오브젝트의 getObject() 메소드를 이용해 오브젝트를 가져오고, 이를 빈 오브젝트로 사용한다.</li>
</ul>
<pre><code class="language-java">    public class MessageFactoryBean implements FactoryBean {
        String text;

        public void setText(Strint text) {
            this.text = text);
        }

        public Message getObject() throws Exception {
            return Message.newMessage(this.text);
        }
    }</code></pre>
</br>

<h3 id="다이내믹-프록시를-만들어주는-팩토리-빈">다이내믹 프록시를 만들어주는 팩토리 빈</h3>
<ul>
<li><p>팩토리 빈을 사용하면 다이내믹 프록시 오브젝트를 스프링의 빈으로 만들어줄 수 있다.</p>
<ul>
<li><p>getObject() 메소드에 다이내믹 프록시 오브젝트를 만들어주는 코드를 넣으면 된다.</p>
<pre><code class="language-java">public class MessageFactoryBean implements FactoryBean {
  public Object getObject() throws Exception { 
      TransactionHandler txHandler = new TransactionHandler();
      txHandler.setTarget(target);
      txHandler.setTransactionManager(transactionManager);
      txHandler.setPattern(pattern);
      return Proxy.newProxyInstance( getClass().getClassLoader(),new Class[] { serviceInterface }, txHandler);
  }
}</code></pre>
</li>
</ul>
</li>
</ul>
<ul>
<li><p>팩토리 빈은 타깃 오브젝트인 UserServiceImpl에 대한 레퍼런스를 프로퍼티를 통해 DI 받아야 한다.</p>
<p>  → TransactionHandler에게 타깃 오브젝트를 전달해주기 위함</p>
</li>
</ul>
<p><img src="https://github.com/user-attachments/assets/486180e6-4cad-4db9-b496-52157f13f155" alt="image 7"></p>
</br>

<h2 id="635-프록시-팩토리-빈-방식의-장점과-한계">6.3.5 프록시 팩토리 빈 방식의 장점과 한계</h2>
<h3 id="프록시-팩토리-빈의-한계">프록시 팩토리 빈의 한계</h3>
<ol>
<li><p>프록시를 통해 타깃에 부가기능을 제공하는 것은 메소드 단위로 일어나는 일이다.</p>
<ol>
<li><p>하나의 클래스 안에 존재하는 여러 개의 메소드에 부가기능을 한 번에 제공하는 것은 가능하지만, 여러 클래스에 공통적인 부가기능을 제공하는 것은 불가능하다.</p>
<p> → 프록시 팩토리 빈의 중복설정으로 이어질 수 있다. </p>
</li>
</ol>
</li>
<li><p>같은 타깃 오브젝트에 트랜잭션 프록시 뿐만 아니라, 다른 프록시도 추가하고 싶다면 프록시 팩토리 빈 설정이 부가기능의 개수만큼 늘어나게 된다. </p>
</li>
<li><p>Handler 오브젝트가 팩토리 빈 개수만큼 늘어난다. </p>
<ol>
<li>타깃 오브젝트를 프로퍼티로 갖고 있으므로, 타깃 오브젝트가 달라질 때마다 새로운 Handler 오브젝트를 만들어야 한다. </li>
</ol>
</li>
</ol>
</br>

<hr>
<h1 id="64-스프링의-프록시-팩토리-빈">6.4 스프링의 프록시 팩토리 빈</h1>
<p>스프링은 이런 문제에 어떤 해결책을 제시할까?</p>
<h2 id="641-proxyfactorybean">6.4.1 ProxyFactoryBean</h2>
<blockquote>
<p>스프링은 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈을 제공한다.</p>
</blockquote>
<ul>
<li><p>스프링의 ProxyFactoryBean은 프록시 생성 작업만 담당하고, 부가기능은 별도의 빈에 둘 수 있다.</p>
<ul>
<li>부가기능은 <code>MethodInterceptor</code> 인터페이스를 구현해 만든다.</li>
</ul>
</li>
<li><p>MethodInterceptor와 InvocationHandler의 차이</p>
<ul>
<li><p><code>InvocationHandler</code>의 invoke() 메소드가 타깃 오브젝트에 대한 정보를 제공하지 않기 때문에 InvocationHandler를 구현한 클래스는 타깃을 알고 있어야 한다.</p>
</li>
<li><p><code>MethodInterceptor</code>의 invoke() 메소드는 ProxyFactoryBean으로부터 타깃 오브젝트에 대한 정보까지도 함께 제공받는다.</p>
<pre><code class="language-java">  static class UppercaseAdvice implements MethodInterceptor { 
          public Object invoke(MethodInvocation invocation) throws Throwable { 
                  String ret = (String)invocation.proceed();
                  return ret.toUpperCase();
          } 
  }</code></pre>
</li>
</ul>
</li>
</ul>
</br>

<h3 id="어드바이스-타깃이-필요-없는-순수한-부가기능">어드바이스: 타깃이 필요 없는 순수한 부가기능</h3>
<ul>
<li><p><code>MothodInvocation</code>은 일종의 콜백 오브젝트로, proceed() 메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행해주는 기능이 있다.</p>
<ul>
<li>일종의 공유가능한 템플릿처럼 동작한다.</li>
<li>ProxyFactoryBean은 템플릿 역할을 하는 MethodInvocation을 싱글톤으로 두고 공유할 수 있다.</li>
</ul>
</li>
<li><p>ProxyFactoryBean에는 여러 개의 MethodInvocation을 추가할 수 있다.</p>
<p>  → ProxyFactoryBean 하나만으로 여러 개의 부가기능을 제공하는 프록시를 만들 수 있다. </p>
</li>
<li><p><code>어드바이스</code> 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트</p>
</li>
</ul>
</br>


<h3 id="포인트컷-부가기능-적용-대상-메소드-선정-방법">포인트컷: 부가기능 적용 대상 메소드 선정 방법</h3>
<aside>

<p>MethodInterceptor 오브젝트는 타깃 정보를 갖고 있지 않기 때문에 스프링의 싱글톤 빈으로 등록된다. </p>
<p>그렇다면 부가기능을 적용하고 싶은 메소드를 어떻게 선별해야할까?</p>
</aside>

<ul>
<li><p>MethodInterceptor는 <a href="https://www.notion.so/e1ab199f006045b1bd6b0a8eecb9adfb?pvs=21">InvocationHandler</a>와 달리 클라이언트로 받는 요청을 일일이 전달받을 필요가 없다.</p>
</li>
<li><p>MethodInterceptor에는 순수한 부가기능 제공 코드만 남겨두고 <code>프록시</code>에 부가기능을 적용할 메소드를 선택하는 기능을 넣자.</p>
<ul>
<li>프록시의 핵심 가치는 타깃을 대신해 클라이언트의 요청을 받는데 있으므로, 메소드 선별기능은 다시 프록시에서 분리하는게 낫다.</li>
</ul>
</li>
<li><p>스프링의 ProxyFactoryBean 방식은 두 가지 확장기능인 부가기능과 메소드 선정 알고리즘을 활용하는 유연한 구조를 제공한다.</p>
</li>
</ul>
<p><img src="https://github.com/user-attachments/assets/555faf89-6834-48a7-ade8-ba63b6f4cf6a" alt="image 8"></p>
<ul>
<li><p>어드바이스와 포인트컷은 모두 프록시에 DI로 주입돼서 사용된다.</p>
<pre><code>  → 여러 프록시에서 공유가능하도록 만들어지기 때문에 스프링 빈으로 등록 가능하다.</code></pre><ol>
<li>프록시는 클라이언트로 요청을 받으면 먼저 포인트컷에게 부가기능을 부여할 메소드인지 확인해달라는 요청을 보낸다.</li>
<li>확인받으면 프록시는 MethodInterceptor타입의 어드바이스를 호출한다. <ol>
<li>어드바이스는 타깃에 의존하지 않도록 일종의 템플릿 구조로 설계되어 있다. </li>
</ol>
</li>
<li>어드바이스가 부가기능을 부여하는 중에 타깃 메소드 호출이 필요하면 프록시로부터 전달받은 MethodInvocation 타입 콜백 오브젝트의 proceed() 메소드를 호출한다. </li>
<li>위임대상인 타깃 오브젝트의 레퍼런스를 갖고 있고 이를 이용해 타깃 메소드를 직접 호출하는건 Invocation 콜백이 수행한다. </li>
</ol>
</li>
<li><p><code>어드바이저</code> = 포인트컷(메소드 선정 알고리즘) + 어드바이스(부가기능)</p>
<pre><code class="language-java">  ProxyFactoryBean pfBean = new ProxyFactoryBean();

  pfBean.addAdvisor(new DefaultPointCutAdvisor(pointcut, new UpperCaseAdvice());</code></pre>
<p>  → 포인트컷과 어드바이스를 따로 등록하게 되면 포인트컷을 어떤 어드바이스에 적용할지 애매하기 때문</p>
</li>
</ul>
</br>

<hr>
<h1 id="65-스프링-aop">6.5 스프링 AOP</h1>
<h2 id="651-자동-프록시-생성">6.5.1 자동 프록시 생성</h2>
<p>프록시 팩토리 빈 방식의 접근 방법의 한계를 되짚어보자.</p>
<ol>
<li><p>부가기능이 타깃 오브젝트마다 새로 만들어지는 문제</p>
<p> → 스프링 ProxyFactoryBean의 어드바이스를 통해 해결</p>
</li>
<li><p>부가기능 적용이 필요한 타깃 오브젝트마다 비슷한 내용의 ProxyFactoryBean 설정 정보를 추가해야함</p>
<p> → ?</p>
</li>
</ol>
</br>

<h3 id="중복문제의-접근-방법">중복문제의 접근 방법</h3>
<aside>

<p>변하지 않는 타깃으로의 위임과 부가기능 적용 여부 판단이라는 부분은 코드 생성 기법을 이용하는 다이내믹 프록시 기술에 맡기고, 변하는 부가기능 코드는 별도로 만들어서 다이내믹 프록시 생성 팩토리에 DI로 제공하는 방법을 사용했다. </p>
</aside>

<p>한 번에 여러 개의 빈에 프록시를 적용하기 위해 ProxyFactoryBean 설정 문제는 어떻게 해결해야 할까?</p>
</br>

<h3 id="빈-후처리기를-이용한-자동-프록시-생성기">빈 후처리기를 이용한 자동 프록시 생성기</h3>
<blockquote>
<p>스프링은 OCP의 가장 중요한 요소인 <strong>유연한 확장</strong> 개념을 스프링 컨테이너 자신에게도 적용하고 있다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[SecurityContextHolder 동작원리 이해하기]]></title>
            <link>https://velog.io/@yul_ee/SecurityContextHolder%EC%99%80-LocalThread</link>
            <guid>https://velog.io/@yul_ee/SecurityContextHolder%EC%99%80-LocalThread</guid>
            <pubDate>Sat, 14 Sep 2024 08:55:18 GMT</pubDate>
            <description><![CDATA[<h2 id="글-작성-배경">글 작성 배경</h2>
<p>나를 포함한 소마 15기 연수생 4명과 함께 6월부터 토비의 스프링 스터디를 진행하고 있다. 스프링 스터디이다보니 책 내용 이외에도 스프링과 관련한 이런 저런 이야기를 많이 나누게 된다. 얼마 전에는 스프링 시큐리티의 SecurityContextHolder의 동작 원리와 관련해 이야기를 나눌 기회가 있었는데, 관련 코드를 작성해본 경험이 있음에도 내부의 동작 방식에 무지했던 것이 부끄러워 뒤늦게라도 공부한 내용을 기록해보고자 한다. </p>
<h2 id="기존에-가지고-있던-의문-securitycontext의-전역적-사용">기존에 가지고 있던 의문: SecurityContext의 전역적 사용</h2>
<p>이전에 인증 로직을 구현하면서 SecurityContextHolder의 동작에 대해 가지고 있던 궁금증이 있다. jwt 토큰을 이용해 인증객체를 생성한 뒤 이 인증객체를 전역적으로 사용하기 위해 SecurityContextHolder의 컨텍스트에 인증객체를 저장하는데, 이때 전역적으로 사용한다는게 어떤 의미인지 잘 와닿지가 않았던 것이다.</p>
<p>아래 코드는 현재 진행중인 프로젝트의 코드로, jwt 토큰을 이용해 API 요청을 보낼 때마다 SecurityContextHolder에 인증객체를 저장하게끔 구현되어 있다. 이렇게 매 요청마다 인증객체를 저장할거라면, SecurityContextHolder가 왜 필요한걸까?
</br></p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtVerifier jwtVerifier;
    private final JwtService jwtService;

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        List&lt;RequestMatcher&gt; matchers = new ArrayList&lt;&gt;();
        matchers.add(new AntPathRequestMatcher(&quot;/customer/product/**&quot;));
        matchers.add(new AntPathRequestMatcher(&quot;/login/**&quot;));
        matchers.add(new AntPathRequestMatcher(&quot;/reissue&quot;));

        return matchers.stream()
            .anyMatch((matcher -&gt; matcher.matches(request)));
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {

        try {
            String accessToken = request.getHeader(&quot;Authorization&quot;);

            if (accessToken == null || !accessToken.startsWith(&quot;Bearer &quot;)) {
                throw new JwtTokenException(JwtErrorCode.INVALID_TOKEN);
            }

            accessToken = accessToken.substring(7);
            if (jwtVerifier.isAccessTokenValid(accessToken)) {
                Authentication auth = jwtService.getAuthentication(accessToken);
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        } catch (ExpiredJwtException e) {
            throw new JwtTokenException(JwtErrorCode.EXPIRED_TOKEN);
        } catch (JwtException e) {
            throw new JwtException(e.getMessage(), e);
        }
        filterChain.doFilter(request, response);
    }
}</code></pre>
</br>

<h3 id="전역적으로-사용한다는-의미">전역적으로 사용한다는 의미</h3>
<p>SecurityContextHolder를 전역적으로 사용가능하다는 것은 <strong>하나의 API 요청</strong>에서 그렇다는 의미이다. 한 요청에서 여러 보안 체크를 하면서 시큐리티 필터 체인이 동작하게 되는데, 이때 SecurityContextHolder에 저장된 인증 정보를 전역적으로 참조한다는 의미인 것이다. 그리고 stateless한 jwt 인증 방식에서는 매 요청마다 토큰을 검증하고 인증객체를 생성하는 코드가 반복되는 것이 자연스럽다.
</br></p>
<h2 id="또-다른-의문-spring-security는-사용자를-어떻게-구분할까">또 다른 의문: Spring Security는 사용자를 어떻게 구분할까?</h2>
<p>SecurityContextHolder는 인증이 완료된 유저의 정보를 저장하는 저장소의 역할을 한다. 스프링 시큐리티는 이 저장소에 인증객체가 어떻게 저장되는지에는 관심이 없으며, 인증객체가 담겨져 있다면 인증된 사용자로 간주하고 사용한다. 
<img src="https://velog.velcdn.com/images/yul_ee/post/bbe37876-3f40-49e3-8706-28e51d5fc58d/image.png" alt=""></p>
</br>

<p>사용자가 인증되었음을 나타내기 위해서 우리는 SecurityContextHolder를 직접 설정하게 된다. 이때 SecurityContextHolder는 <code>new</code>를 통해 매번 새로운 인스턴스가 생성되는 것이 아님을 확인할 수 있다. 
그런데 그렇다면, 매번 다른 유저가 API 요청을 동시다발적으로 보내게 될 텐데 SecurityContextHolder에 저장된 인증객체도 매번 새로운 값으로 덮어씌워지는 건 아닐까? 하나의 유저 당 하나의 SecurityContextHolder를 가진다는 건 어떻게 보장할 수 있는걸까?</p>
<pre><code class="language-java">public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {

            accessToken = accessToken.substring(7);
            if (jwtVerifier.isAccessTokenValid(accessToken)) {
                Authentication auth = jwtService.getAuthentication(accessToken);
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
     }
}  

public class JwtService {
   public Authentication getAuthentication(String token) {
          String memberId = String.valueOf(jwtProvider.getMemberIdFromToken(token));
          CustomUserDetails userDetails = customUserDetailsService.loadUserByUsername(memberId);

          return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
      }
}      </code></pre>
</br>

<h3 id="threadlocal을-통한-동시성-보장">ThreadLocal을 통한 동시성 보장</h3>
<p><img src="https://velog.velcdn.com/images/yul_ee/post/473623ff-16bd-4fe5-9e2c-3a4b67cfba9c/image.png" alt=""></p>
<p>SecurityContextHolder의 내부 구현 코드를 확인해보자. <code>initializeStrategy()</code> 메소드를 보면 개발자가 특별하게 전략을 설정하지 않는 경우에는 기본적으로 <code>MODE_THREADLOCAL</code>이 설정됨을 알 수 있다. 즉, SecurityContextHolder가 내부적으로 사용하는 SecurityContext는 ThreadLocal에 저장된다. 
따라서 여러 사용자가 동시다발적으로 API 요청을 보내더라도 각 요청마다 서로 다른 스레드가 할당되고 스레드마다 독립적인 SecurityContext를 갖게 되기 때문에 유저간 인증객체는 서로 간섭받지 않는다는 걸 보장할 수 있게 되는 것이다.</p>
<pre><code class="language-java">public class SecurityContextHolder {
    public static final String MODE_THREADLOCAL = &quot;MODE_THREADLOCAL&quot;;
    public static final String MODE_INHERITABLETHREADLOCAL = &quot;MODE_INHERITABLETHREADLOCAL&quot;;
    public static final String MODE_GLOBAL = &quot;MODE_GLOBAL&quot;;
    private static final String MODE_PRE_INITIALIZED = &quot;MODE_PRE_INITIALIZED&quot;;
    public static final String SYSTEM_PROPERTY = &quot;spring.security.strategy&quot;;
    private static String strategyName = System.getProperty(&quot;spring.security.strategy&quot;);
    private static SecurityContextHolderStrategy strategy;
    private static int initializeCount = 0;

    public SecurityContextHolder() {
    }

    private static void initialize() {
        initializeStrategy();
        ++initializeCount;
    }

    private static void initializeStrategy() {
        if (&quot;MODE_PRE_INITIALIZED&quot;.equals(strategyName)) {
            Assert.state(strategy != null, &quot;When using MODE_PRE_INITIALIZED, setContextHolderStrategy must be called with the fully constructed strategy&quot;);
        } else {
            if (!StringUtils.hasText(strategyName)) {
                strategyName = &quot;MODE_THREADLOCAL&quot;;
            }

            if (strategyName.equals(&quot;MODE_THREADLOCAL&quot;)) {
                strategy = new ThreadLocalSecurityContextHolderStrategy();
            } else if (strategyName.equals(&quot;MODE_INHERITABLETHREADLOCAL&quot;)) {
                strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
            } else if (strategyName.equals(&quot;MODE_GLOBAL&quot;)) {
                strategy = new GlobalSecurityContextHolderStrategy();
            } else {
                try {
                    Class&lt;?&gt; clazz = Class.forName(strategyName);
                    Constructor&lt;?&gt; customStrategy = clazz.getConstructor();
                    strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
                } catch (Exception var2) {
                    Exception ex = var2;
                    ReflectionUtils.handleReflectionException(ex);
                }

            }
        }
    }</code></pre>
<pre><code class="language-java">final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
  private static final ThreadLocal&lt;Supplier&lt;SecurityContext&gt;&gt; contextHolder = new ThreadLocal();
}</code></pre>
</br>

<h3 id="threadlocal이-아닌-다른-전략을-사용하는-경우는-언제일까">ThreadLocal이 아닌 다른 전략을 사용하는 경우는 언제일까?</h3>
<p>기본적으로 ThreadLocal 전략이 설정되었기 때문에 사용자마다 독립적인 SecurityContext를 가지며, 실제로 그렇게 동작하는 것이 합리적이라는 생각이 든다. 그런데 위 코드를 살펴보다 보면 <code>MODE_INHERITABLETHREADLOCAL</code>, <code>MODE_GLOBAL</code>과 같은 다른 전략을 선택할 수도 있음을 확인할 수 있는데 이것들은 무엇이고 언제, 왜 사용하게 되는건지 궁금해졌다. </p>
<ol>
<li>MODE_INHERITABLETHREADLOCAL</li>
</ol>
<ul>
<li>부모-자식 스레드 간의 SecurityContext를 상속할 필요가 있을 때 사용한다. </li>
<li>기본적으로 ThreadLocal 전략에서는 부모-자식 스레드 간에 SecurityContext를 공유하지 않는다. 하지만 해당 전략을 사용하게 되면 자식 스레드가 부모 스레드의 SecurityContext를 상속받기 때문에 자식 스레드에서도 인증이 유지된다.</li>
<li>구체적으로 어떤 경우에 이런 필요성이 대두되는지는 아직 파악하지 못했다. </br>
</li>
</ul>
<ol start="2">
<li>MODE_GLOBAL</li>
</ol>
<ul>
<li>내가 처음 이해했던 것처럼 하나의 SecurityContext를 모든 스레드가 공유하게끔 설정할 때 해당 전략을 사용한다. </li>
<li>ThreadLocal 전략에서는 각 스레드마다 독립적인 SecurityContext를 관리하지만, 이 전략에서는 모든 스레드가 동일한 SecurityContext를 공유하도록 보장하므로 이름 그대로 전역적인 모드를 지원하는 것이다.</li>
<li>실제 배포 환경에서 해당 전략을 사용할 가능성은 0에 수렴할 것 같지만 아마 특수한 테스트 환경 등에서 전역적인 사용이 필요한 경우도 있지 않을까 생각해본다.</li>
</ul>
</br>

<h2 id="threadlocal-사용-시-주의사항">ThreadLocal 사용 시 주의사항</h2>
<p><em>*김영한님의 스프링 핵심 원리 - 고급편 참고</em></p>
<p>ThreadLocal을 스레드 풀과 함께 사용할 때 스레드 재사용과 관련해 주의해야 할 점이 있다고 한다. 이를 그림으로 이해해보자. </p>
<ol>
<li><p>사용자 A의 요청
<img src="https://velog.velcdn.com/images/yul_ee/post/71074fb8-88c4-43f7-a379-2207204c1ace/image.png" alt=""></p>
</li>
<li><p>사용자 A의 요청 종료
<img src="https://velog.velcdn.com/images/yul_ee/post/4e229940-4595-4e39-becf-f6138154fa25/image.png" alt="">
WAS는 사용이 끝난 ThreadA를 스레드 풀에 반환한다. 이는 스레드 생성 비용이 비싸기 때문에, 스레드를 나중에 재사용하기 위함이다. 따라서 ThreadA는 스레드 풀에 계속해서 살아있게 되고, ThreadA 전용 보관소에 userA에 대한 정보도 함꼐 살아있게 된다. </p>
</li>
<li><p>사용자 B의 요청
<img src="https://velog.velcdn.com/images/yul_ee/post/fcffd013-bc05-4d5b-aa1c-650a29144ef0/image.png" alt="">
만약 ThreadA가 재할당되면, ThreadA가 스레드 로컬 조회 시 사용자 A에 대한 저장소에서 사용자 A에 대한 정보가 반환되는 심각한 문제가 발생할 수 있다. </p>
</li>
</ol>
</br>
이런 문제를 해결하기 위해서 사용자 A의 요청이 끝나는 시점에 스레드 로컬의 값을 ThreadLocal.remove()를 통해 꼭 제거해주어야 한다고 한다. 


</br>

<h3 id="내-코드에도-적용해야-할까">내 코드에도 적용해야 할까?</h3>
<p>내가 작성한 JwtAuthenticationFilter 클래스는 OncePerRequestFilter를 상속받고 있다. 매 요청마다 해당 필터가 동작하면서 인증객체를 생성하고, ThreadLocal 매커니즘으로 동작하는 SecurityContext에 이 인증객체를 저장하고 있다. 그럼 위에서 살펴본 ThreadLocal 사용 시 발생할 수 있는 문제점을 예방하기 위해 OncePerRequest 필터에서 스레드 로컬을 비워주는 코드를 작성해야 하는 건 아닐까?</p>
<p>하지만 스프링 시큐리티는 기본적으로 요청이 끝난 뒤에 SecurityContext를 자동으로 정리해준다고 한다. 따라서 명시적으로 코드를 작성할 필요는 없어보인다.</p>
<h2 id="마무리">마무리</h2>
<p>평소에 코드를 작성하면서 조금이라도 의문이 들거나 궁금한 점이 생기면 그걸 이해하고 넘어가려고 습관을 들여야겠다는 생각이 든다. 이번에 글을 작성하며 작은 의문 하나 하나를 해소하는 과정에서 생각지도 못했던 단어, 개념이 등장하고 그걸 공부하다보니 조금씩 지식이 확장되는 즐거운 경험을 했던 것 같다 ✨</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security + OAuth2 Client로 카카오 로그인 연동하기 (1)]]></title>
            <link>https://velog.io/@yul_ee/Spring-Security-OAuth2-Client%EB%A1%9C-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yul_ee/Spring-Security-OAuth2-Client%EB%A1%9C-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 01 Sep 2024 16:11:54 GMT</pubDate>
            <description><![CDATA[<p>현재 개발 중인 프로젝트에 카카오 로그인 연동이 필요해 약 한 달 정도의 긴 기간 동안 Spring Security + OAuth 2.0 Client와 힘든 싸움을 했었다.
연동을 하기까지 많은 공부가 필요했고 시간도 많이 잡아먹으면서 힘들게 연동에 성공했지만, 지금 프로젝트에는 적합하지 않은 몇가지 이유가 생겨서 현재는 라이브러리를 제거하고 직접 구현한 코드로 전부 교체해둔 상황이다. 그렇지만 OAuth2.0 Client를 사용하면서 배운 점이 많고, 또 무엇보다 구현하면서 참고할 레퍼런스가 많지 않아 어려움을 겪었기 때문에 누군가에게 도움이 되길 바라며 기록으로 남겨보고자 한다.</p>
<p>(1)에서는 OAuth2.0 Client에 대한 개념적 이해
(2)에서는 OAuth2.0 Client를 사용했을 때 필터체인의 동작 흐름 및 각 필터의 역할
(3)에서는 OAuth2.0 Client를 사용해서 카카오 로그인을 연동한 실제 코드
(4)에서는 라이브러리를 사용하지 않고 직접 카카오 로그인을 연동한 코드</p>
<p>총 네번에 나눠 기록하고자 하고, (1)에서는 간단히 라이브러리를 이해한 내용을 작성해보았다.
<br></p>
<hr>
<h3 id="spring-security--oauth-20-client">Spring Security + OAuth 2.0 Client</h3>
<p>Spring Security는 HTTP 요청을 처리하는 여러 필터의 체인으로 구성된다. 각 필터는 특정한 보안 작업을 수행하게 되는데, OAuth 2.0 Client 라이브러리는 이 체인에 OAuth2와 관련된 필터들을 추가하여 클라이언트의 인증 과정을 처리하는데 도움을 준다.</p>
<p>OAuth2와 관련된 대표적인 필터로는 <code>OAuth2LoginAuthenticationFilter</code>가 있는데, 여기서는 간단히 언급만 하고 필터에서 처리하는 동작이나 전체적인 흐름에 대해서는 (2)편에서 자세히 녹여보고자 한다.</p>
<h3 id="oauth20-client의-역할은">OAuth2.0 Client의 역할은?</h3>
<blockquote>
<p>The OAuth 2.0 Client features provide support for the Client role as defined in the OAuth 2.0 Authorization Framework.</p>
</blockquote>
<p><a href="https://docs.spring.io/spring-security/reference/servlet/oauth2/client/index.html">공식문서</a>에서 <code>OAuth2.0 Client</code>는 OAuth2.0 인증 프레임워크에 정의된 클라이언트의 역할을 지원한다고 소개되고 있다. OAuth에서 정의하고 있는 역할을 이해하고자 개략적으로 그렸던 그림이 있어 함께 첨부한다.
<img src="https://velog.velcdn.com/images/yul_ee/post/d718b7bd-9c5b-45d3-9305-5ebcafa82dcd/image.png" alt=""></p>
<ul>
<li><code>Resource Owner</code>: 자원(여기서는 사용자 정보)에 대한 접근권한을 가지고 있는 소유자를 말한다. 카카오 사용자인 홍길동이 여기 해당한다. </li>
<li><code>Resource Server</code>: 카카오 계정 정보와 같은 보호된 자원를 가지고 있는 주체로 access token이 유효한 경우에 이 자원을 제공한다. 카카오 API 서버가 여기 해당한다.</li>
<li><code>Client</code>: Resource Owner의 승인을 받아 카카오 계정 정보에 접근하고자 하는 외부 어플리케이션을 말한다. 우리는 여기 해당한다. </li>
<li><code>Authorization Server</code>: Resource Owner가 카카오 계정에 로그인 했는지 여부를 확인하고, Resource Owner의 승인을 받아 access token을 Client에 제공하는 역할을 한다. 카카오 인증 서버가 여기 해당한다.  <br>

</li>
</ul>
<p>OAuth 2.0 Client 라이브러리는 <code>Client</code>의 역할을 지원한다고 했는데, Client가 해야 할 역할은 아래와 같이 크게 세가지로 구분된다.</p>
<figure>
    <img src="https://velog.velcdn.com/images/yul_ee/post/f1f7f4e6-bfcf-4275-8691-8e067fcf3e36/image.png" width="50%">
  <figcaption>출처 카카오 로그인 REST API 문서</figcaption>  
  </figure>

<ol>
<li>인가 코드 요청 및 받기</li>
<li>인가 코드와 함께 엑세스토큰 요청 및 받기</li>
<li>엑세스토큰과 함께 사용자 정보 요청 및 받기</li>
</ol>
<p>즉, Spring Security의 OAuth2 Client 라이브러리를 사용하면 Spring Security 필터 체인에서 동작하는 여러 필터를 통해 위와 같은 OAuth 2.0 인증 프로세스를 전부 도맡아 처리해준다고 이해하면 된다. </p>
<br>

<h3 id="oauth20-client의-장단점">OAuth2.0 Client의 장단점</h3>
<p>라이브러리를 사용해보고 느낀 장단점은 다음과 같다. </p>
<p>장점</p>
<ul>
<li>인가 코드 받기, 엑세스 토큰 받기, 사용자 정보 조회 등 일련의 과정을 수행하기 위해 Kakao API Server, Kakao Authorization Server로 API 요청을 직접 보낼 필요가 없다. 따라서 HTTP 요청을 보내기 위한 별도의 라이브러리(ex: WebFlux)를 추가할 필요도 없다. </li>
<li>application.yml 등의 파일에 <code>provider</code> 정보만 추가하는 등 간단한 설정만 하면 된다.</li>
</ul>
<p>단점</p>
<ul>
<li>일련의 인증 과정이 Spring Security와 밀접하게 엮여서 필터 단위로 동작하는데, Spring Security를 사용해본 적이 없다면 초반엔 이 개념과 흐름을 이해하기 어렵고 디버깅이 쉽지 않다.</li>
<li><strong>인증 과정에서 제어권을 라이브러리가 가지며, 인증 흐름을 커스터마이징 할 수 없다.</strong></li>
</ul>
<br>
마지막 문장에 대한 부연설명을 하자면 /oauth2/authorization/kakao로 인증 흐름이 시작됨과 동시에 인가 코드 요청, 토큰 요청, 사용자 정보 조회가 라이브러리에 의해 자동으로 진행되며, 이 인증 흐름에서 read only 파일로 구성된 시큐리티 필터 체인을 연속적으로 통과하게 되기 때문에 인증 흐름 중간에 끼어들거나 커스터마이징 하는 것이 불가능하다고 이해하면 될 것 같다.

<p>예를 들면, 인가 코드 요청/받기는 클라이언트에서 직접 수행한 뒤에 서버에 인가코드와 함께 로그인 요청을 보내서 서버는 토큰 요청하기/받기 부터 수행하겠다와 같이 흐름을 직접 제어하는 것이 불가능해진다.
<br></p>
<hr>
<p>(2)편에서는 실제로 OAuth2.0 Client를 사용했을 때 필터체인이 어떻게 동작하는지 그 흐름을 이해해보고자 한다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SpringBoot 프로젝트에 AWS Cognito 연동하기 (1)]]></title>
            <link>https://velog.io/@yul_ee/SpringBoot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-AWS-Cognito-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@yul_ee/SpringBoot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-AWS-Cognito-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Wed, 24 Jul 2024 15:20:47 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기-전-사담">들어가기 전 사담</h2>
<p>올해 4월부터 소프트웨어 마에스트로 15기 과정에 참여하게 되었는데, 4월부터 6월까지 약 3개월이라는 긴 시간동안 프로젝트 기획 및 기획 검증의 시간을 가졌다.</p>
<p>기획에 꽤 많은 시간을 쏟다보니 (기획에 이렇게 공을 들여본 프로젝트는 처음이다 .. !) 개발에 투자할 시간이 줄어들어 일정이 살짝 빠듯해졌다. 프로토타입 출시까지 남은 약 2주의 시간 동안 빠르게 개발이 이루어져야 하는 상황이다. </p>
<p>나는 현재 카카오 로그인 및 회원가입 개발을 맡고 있는데, 스프링 시큐리티와 함께 OAuth2 Client 라이브러리를 사용하는 과정에서 (많은) 시행착오를 겪어 생각보다 개발이 지연되었다. 그래도 다행히 프론트엔드와 연동 테스트를 거쳐 <strong>무사히</strong> 사용자 정보 가져오기를 마쳤다!</p>
<h2 id="cognito-도입기">Cognito 도입기</h2>
<p>카카오 로그인 과정에서 엑세스 토큰과 리프레시 토큰을 발급받기는 하지만, 보안상의 이유로 서비스 API를 호출할 때 카카오 토큰을 계속해서 사용하는 것은 권장되지 않는다고 한다. 따라서 카카오 로그인이 끝나는 시점에 서비스 내에서 자체적으로 엑세스 토큰과 리프레시 토큰을 생성해주고자 한다. </p>
<p>처음에는 토큰 관련한 로직을 전부 직접 개발하려고 했지만, 앞서 사담에서 언급했듯이 개발 일정이 빠듯한 와중에 이건 좋은 선택이 아닌 것 같았다. 그리고 무엇보다 실사용자를 유치하고자 하는 서비스에서 보안 상 구멍이라도 생긴다면 ..? 🫨</p>
<p>이때 AWS Cognito를 사용하게 되면 Cognito에서 제공해주는 API를 사용해 보다 간편하고 안전하게 인가 처리를 할 수 있다고 해 고민끝에 도입해보기로 결정하게 되었다. (그리고 무엇보다 멘토님의 추천이 있었다!🤩)</p>
<p>또 사용자 50,000명이 넘어가기 전까지는 과금 없이 Cognito에서 제공하는 서비스를 계속해서 무료로 사용할 수 있다는 점도 큰 메리트로 다가왔다.</p>
<h2 id="그래서-cognito란">그래서, Cognito란?</h2>
<p>Cognito는 인증/인가, 로그인, 회원가입, 자격증명 등 회원관리에 필요한 모든 기능들을 총망라하여 지원해주는 AWS 서비스 중 하나이다. AWS Cognito에서 제공하는 기능은 크게 두 가지로, 사용자 풀(User Pool)과 자격 증명 풀(Identity Pool)이 있다.</p>
<p><strong>사용자 풀</strong>은 말 그대로 사용자 정보를 저장하고 있는 저장소의 역할을 한다. 다양한 방식의 회원가입과 로그인을 지원하고, 로그인 한 사용자에게는 토큰을 발급해 자격 증명을 수행할 수 있게 도와준다. </bn></p>
<p><strong>자격 증명 풀</strong>은 사용자 풀에 등록되어 있는 사용자의 AWS 인프라 접근 권한을 관리할 수 있도록 도와준다. 사용자 풀을 생성하는 과정이 선행시 되어야 한다는 특징이 있다. </p>
<h2 id="도입-환경">도입 환경</h2>
<ul>
<li>Spring Boot 3.3.1</li>
<li>Spring Security + OAuth2 Client 라이브러리를 사용하여 카카오 인증/인가 구현</li>
<li>자체 DB를 구축하여 사용자 정보 저장
  → 회원가입 시 필요한 정보들을 모두 사용자 풀에 저장하진 않는다. 사용자 풀은 오로지 <strong>토큰을 발급</strong>하는 목적으로만 사용한다. </bn>
</bn>

</li>
</ul>
<hr>
<h2 id="aws-cognito-연동-과정">AWS Cognito 연동 과정</h2>
<blockquote>
<p>Cognito를 도입하게 된 주요 목적이 토큰 발행 및 관리인 만큼, 이번에는 사용자 풀을 생성하고 프로젝트와 연동하는 방법에 대해서만 다루고자 한다.</p>
</blockquote>
<h3 id="aws-cognito-사용자-풀-생성">AWS Cognito 사용자 풀 생성</h3>
<ol>
<li><a href="https://ap-northeast-2.console.aws.amazon.com/cognito/v2/idp/user-pools?region=ap-northeast-2">AWS Cognito 사용자 풀</a>에 접속한다. </li>
<li>사용자 풀 생성<img src="https://velog.velcdn.com/images/yul_ee/post/ec04566b-a86a-44eb-ab18-f015fc7d9a16/image.png" alt=""></li>
<li>로그인 환경 구성</li>
</ol>
<ul>
<li>공급자 유형은 기본 옵션으로 지정되어 있는 <code>Cognito 사용자 풀</code>을 선택한다. 만약 자격 증명 풀도 함께 사용하고자 한다면 <code>연동 자격 증명 공급자</code>를 함께 선택하면 된다.</li>
<li><code>사용자 풀 로그인 옵션</code>은 사용자 풀에서 개별 사용자를 구별하기 위해 사용할 고유값이라고 생각하면 된다. 여기서 선택한 옵션이 사용자 풀에 중복되게 존재하면 사용자를 등록할 수 없기 때문에 나는 사용자 이름 대신 이메일을 선택했다. 
<img src="https://velog.velcdn.com/images/yul_ee/post/6136ae31-a5fb-4d67-8f4b-262c7b31e37b/image.png" alt=""></li>
</ul>
<ol start="4">
<li>보안 요구 사항 구성
(1) 암호 정책
Cognito를 이용해 회원가입까지 구현할 계획이 아니라면 암호 정책을 사용할 일이 없으니 신경쓰지 않아도 된다. 기본값으로 선택해줬다.
<img src="https://velog.velcdn.com/images/yul_ee/post/876d7a1f-ae77-4406-90e7-68d923e331e3/image.png" alt="">
(2) 멀티 팩터 인증
Cognito를 이용해 로그인 프로세스를 처리할 때, 비밀번호 입력 외에도 SMS 인증 등을 추가로 사용하고자 할 때 멀티 팩터 인증을 적용하면 된다. 마찬가지로 사용하지 않으므로 MFA 없음을 선택한다.
<img src="https://velog.velcdn.com/images/yul_ee/post/9829a5a1-45aa-46ed-9605-45a46a92ee1a/image.png" alt="">
(3) 사용자 계정 복구
<img src="https://velog.velcdn.com/images/yul_ee/post/e83ac235-e752-43cf-aa1f-da97d11431e2/image.png" alt=""></li>
<li>가입 환경 구성
(1) 셀프 서비스 가입
활성화 하게 되면 인터넷 상의 모든 사용자가 사용자 풀에 자신을 직접 등록할 수 있게 된다. 필요하지 않은 기능이므로 선택하지 않는다.
<img src="https://velog.velcdn.com/images/yul_ee/post/65c5a7e9-2291-44ba-b951-1cb5178e21b1/image.png" alt="">
(2) 속성 확인 및 사용자 계정 확인
Cognito에서 사용자에게 이메일이나 SMS를 보내 사용자 계정을 확인 및 검증할 수 있다. 다만 이 옵션을 허용할 경우 메시지 전송에 별도의 요금이 추가될 수 있음에 주의한다.
<img src="https://velog.velcdn.com/images/yul_ee/post/063ee31d-4428-4b6a-8bca-36800659e252/image.png" alt="">
(3) 필수 속성 및 사용자 지정 속성
<code>필수 속성</code>은 사용자를 유저 풀에 등록할 때 반드시 포함해야 할 정보를 말한다.  앞서 로그인 옵션으로 이메일을 선택했기 때문에 이메일은 자동으로 포함되었다. 
유저 풀을 DB처럼 사용할 생각이 아니기 때문에 필수 속성을 추가로 등록하지 않았다. 
<code>사용자 지정 속성</code>도 마찬가지의 이유로 추가로 등록하지 않고 넘어간다.
<img src="https://velog.velcdn.com/images/yul_ee/post/bda2dc79-cf85-4c8d-8c80-50233f9d7423/image.png" alt=""></li>
<li>메시지 전송 구성
유저 풀에 등록된 사용자에게 메시지를 전송할 방식을 선택한다. Amazon SES를 사용할 경우 별도의 요금이 부과된다. Cognito를 사용하는 방식을 선택하고 From, Reply-to는 기본값 그대로 둔다.
<img src="https://velog.velcdn.com/images/yul_ee/post/5d922178-a5ff-4f5a-a6ac-190c65111679/image.png" alt=""></li>
<li>앱 통합
(1) 사용자 풀 이름
AWS Cognito에서 유저 풀을 구분하기 위한 용도이다. 어떤 프로젝트/서비스에 사용하는 것인지 잘 구분되도록 지으면 된다.
<img src="https://velog.velcdn.com/images/yul_ee/post/4963794d-6995-4a4b-8c1d-72a61d00177e/image.png" alt="">
(2) 호스팅 인증 페이지
Cognito에서 제공하는 호스팅 UI를 사용해 회원가입 및 로그인을 구현한다면 해당 옵션을 선택한다. 이번 프로젝트에서는 카카오 로그인을 연동하고, 로그인 이후에 백엔드 로직에서만 Cognito를 활용할 계획이므로 선택하지 않았다.
<img src="https://velog.velcdn.com/images/yul_ee/post/8e0abcb2-80e2-4d7e-a4f9-ccdeac767f3f/image.png" alt="">
(3) 초기 앱 클라이언트
Cognito 인증을 <strong>어디에 어떻게</strong> 붙일 것인지를 선택하는 항목이다.
 → 퍼블릭 클라이언트: SPA나 모바일 앱에서 직접 인증을 처리하는 경우
 → 기밀 클라이언트: 백엔드 서버에서 인증을 처리하는 경우</bn>
앱 유형을 구분할 때의 핵심은 `클라이언트 보안키`(Secret Key)를 노출하지 않고 안전하게 관리할 수 있느냐인데, 브라우저 환경에서는 보안키를 안전하게 관리할 수 없으므로 퍼블릭 클라이언트는 보안키를 생성하지 않도록 주의한다.
</bn>
우리 프로젝트에서는 백엔드 서버에서 인증을 처리하고 있으므로 `기밀 클라이언트`를 선택해 주고, 클라이언트 보안키를 생성해 환경변수로 안전하게 처리하고자 한다.
![](https://velog.velcdn.com/images/yul_ee/post/3acd2fbf-2b9d-49bd-95f3-de426a4caf6e/image.png)
(4) 고급 앱 클라이언트 설정
엑세스 토큰, 리프레시 토큰의 만료 시간을 설정할 수 있다. 서비스를 운영해보면서 적절히 조절해주는게 좋을 것 같아 아직 별도로 커스텀하지는 않았다.
![](https://velog.velcdn.com/images/yul_ee/post/071b2ddf-5cad-4b68-b4c1-ba275f086823/image.png)
(5) 속성 읽기 및 쓰기 권한
유저 풀에 등록된 사용자의 속성에 대해 읽기/쓰기 권한을 할당할 수 있는데 별도로 커스텀 할 필요성을 느끼지 못해 마찬가지로 기본값으로 설정하고 넘어간다.
</bn>
</bn></li>
<li>사용자 풀 생성
사용자 풀 생성이 끝났다! 
이제 프로젝트와 연동해 Cognito에서 제공해주는 API를 활용해서 카카오에서 가져온 유저 정보를 사용자 풀에 등록해주고, 토큰을 발급해주는 작업을 하면 된다 🥳
<img src="https://velog.velcdn.com/images/yul_ee/post/e3cc9fd4-cf71-470a-8751-40b72db46175/image.png" alt=""></li>
</ol>
<hr>
<p><a href="https://www.youtube.com/watch?v=BqgCJzSOT2k">[보안/인증] Amazon Cognito를 이용한 백엔드 API 권한 관리 | 배진수, 당근마켓</a>을 참고했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] 중복 제거하기]]></title>
            <link>https://velog.io/@yul_ee/SQL-%EC%A4%91%EB%B3%B5-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yul_ee/SQL-%EC%A4%91%EB%B3%B5-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 11 Feb 2024 05:59:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/59408">https://school.programmers.co.kr/learn/courses/30/lessons/59408</a></p>
</blockquote>
<p>동물 보호소에 들어온 동물의 이름이 몇 개인지 조회하는 쿼리를 작성하는 문제이다.
만약 동물들의 이름이 중복되는 경우에는 하나로 합치고, 이름이 NULL인 경우에는 집계하지 않아야 한다. 
<BR/></p>
<h2 id="코드-및-풀이">코드 및 풀이</h2>
<pre><code class="language-sql">SELECT COUNT(DISTINCT NAME) 
FROM ANIMAL_INS
WHERE NAME IS NOT NULL;
</code></pre>
<p>DISTINCT 키워드를 사용하여 NAME의 중복을 제거하고,
WHERE 절을 사용하여 NAME이 NULL인 경우를 제외한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 16236번: 아기 상어]]></title>
            <link>https://velog.io/@yul_ee/%EB%B0%B1%EC%A4%80-16236%EB%B2%88-%EC%95%84%EA%B8%B0-%EC%83%81%EC%96%B4</link>
            <guid>https://velog.io/@yul_ee/%EB%B0%B1%EC%A4%80-16236%EB%B2%88-%EC%95%84%EA%B8%B0-%EC%83%81%EC%96%B4</guid>
            <pubDate>Sun, 11 Feb 2024 05:46:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://www.acmicpc.net/problem/16236">https://www.acmicpc.net/problem/16236</a></p>
</blockquote>
<BR/>

<h2 id="알고리즘-분류">알고리즘 분류</h2>
<ul>
<li>구현</li>
<li>시뮬레이션</li>
<li>그래프 이론</li>
<li>그래프 탐색</li>
<li>너비 우선 탐색</li>
</ul>
<BR/>

<h2 id="풀이">풀이</h2>
<p>아기 상어가 물고기를 먹기 위해서는 고려해야 할 우선순위들이 있다.
    1. 최단 거리의 물고기를 먹는다.
    2. 최단 거리의 물고기가 여러 마리라면, 가장 위쪽에 있는 물고기를 먹는다. 
    3. 이러한 물고기도 여러 마리라면, 가장 왼쪽에 있는 물고기를 먹는다. </p>
<p>입력받은 이차원 리스트 하나만 사용해서는 이걸 모두 고려한 코드를 작성하기 어려웠다. 따라서 거리를 계산하기 위한 이차원 리스트가 추가로 하나 더 필요했다.</p>
<p>무엇보다 이 풀이의 핵심은 우선순위 기준에 따라 정렬을 해주는 것이라고 생각한다. 
bfs 탐색 시, 물고기를 먹을 수 있다고 판단되면 [거리, x좌표, y좌표] 정보를 리스트에 저장한다. 그리고 우선순위가 높은 순서대로 정렬하여 가장 우선순위가 높은 물고기를 선별한 후, 최종적으로 &#39;먹었다&#39; 라고 처리해주면 되는 것이다. </p>
<p>이 과정을 먹을 수 있는 물고기가 더 이상 없을 때 까지 반복한다.</p>
<br/>

<h2 id="코드">코드</h2>
<pre><code class="language-python">import sys
from collections import deque
input = lambda: sys.stdin.readline().rstrip()

N = int(input())
board = [list(map(int, input().split())) for _ in range(N)]
direct = [(-1, 0), (0, -1), (1, 0), (0, 1)]
queue = deque([])

def bfs(start_x, start_y, size):
    distance = [[0]*N for _ in range(N)]
    visited = [[0]*N for _ in range(N)]
    priority = []

    queue.append([start_x, start_y])
    while queue:
        i, j = queue.popleft()
        visited[i][j] = 1

        for x, y in direct:
            nx = i + x
            ny = j + y

            if N &gt; nx &gt;= 0 and N &gt; ny &gt;= 0 and visited[nx][ny] == 0:
                if size &gt;= board[nx][ny]:
                    queue.append([nx, ny])
                    visited[nx][ny] = 1
                    distance[nx][ny] = distance[i][j] + 1
                if size &gt; board[nx][ny] &gt; 0:
                    priority.append([distance[nx][ny], nx, ny])
    return sorted(priority, key=lambda x:(-x[0], -x[1], -x[2]))

ans = cnt = 0
size = 2
for i in range(N):
    for j in range(N):
        if board[i][j] == 9:
            board[i][j] = 2
            x = i
            y = j

while True:
    shark = bfs(x, y, size)
    board[x][y] = 0
    if len(shark) == 0:
        break

    time, x, y = shark.pop()
    ans += time
    board[x][y] = 0
    cnt += 1

    #아기 상어는 자신의 크기와 같은 수의 물고기를 먹을 때 마다 크기가 1 증가한다
    if cnt == size:
        size += 1
        cnt = 0

print(ans)

</code></pre>
<br/>

<p><span style="color:gray"><a href="https://resilient-923.tistory.com/357">https://resilient-923.tistory.com/357</a> 를 참고하였습니다.</span></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] 정렬하기]]></title>
            <link>https://velog.io/@yul_ee/SQL-%EC%A0%95%EB%A0%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yul_ee/SQL-%EC%A0%95%EB%A0%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 11 Feb 2024 05:22:13 GMT</pubDate>
            <description><![CDATA[<h1 id="order-by">ORDER BY</h1>
<blockquote>
<ul>
<li>ORDER BY 절은 SELECT 문의 맨 끝에 위치한다.</li>
<li>ASC/DESC 키워드를 이용해 오름차순/내림차순 정렬을 할 수 있다.</li>
</ul>
</blockquote>
<br/>

<h3 id="column-name-사용하기">COLUMN NAME 사용하기</h3>
<pre><code class="language-sql">SELECT * FROM TABLE_NAME
ORDER BY COLUMN_NAME;</code></pre>
<br/>

<h3 id="열의-인덱스-사용하기">열의 인덱스 사용하기</h3>
<pre><code class="language-sql">SELECT * FROM TABLE_NAME
ORDER BY 3;</code></pre>
<br/>

<h3 id="여러-기준으로-정렬하기">여러 기준으로 정렬하기</h3>
<pre><code class="language-sql">-- 첫 번째 열을 기준으로 오름차순 정렬한 뒤,
-- 세 번째 열을 기준으로 내림차순 정렬하는 쿼리

SELECT * FROM TABLE_NAME
ORDER BY 1, 3 DESC;</code></pre>
<br/>

]]></description>
        </item>
    </channel>
</rss>