<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코딩블로그</title>
    <link>https://hanbxx.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 10 Aug 2026 03:51:02 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>_hanbxx_</managingEditor>
    <item>
      <title>다익스트라 알고리즘 feat:BOJ1753 최단경로 JAVA</title>
      <link>https://hanbxx.tistory.com/80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다익스트라 알고리즘이란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;매번 방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택하여 한 단계씩 최단 거리를 구해나간다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;음수 간선이 없다면 최적의 해를 찾을 수 있다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;시간 복잡도가 빠르다 . 개선된 다익스트라 알고리즘도 존재하는데 이는 우선순위 큐 사용해서 구현할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BOJ1753 - 최단경로&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1753&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1753&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 개선된 다익스트라 형태인 우선순위큐를 활용해서 풀어야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1727142535702&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;
import java.io.*;

class Main {
    // 간선 정보를 저장하는 Edge 클래스, 우선순위 큐에서 비교를 위해 Comparable 인터페이스 구현
    public static class Edge implements Comparable&amp;lt;Edge&amp;gt; {
        int end;  // 도착 노드
        int cost; // 간선의 비용

        public Edge(int end, int cost) {
            this.end = end;
            this.cost = cost;
        }

        @Override
        public int compareTo(Edge edge) {
            return cost - edge.cost; // 비용을 기준으로 오름차순 정렬
        }
    }

    // 노드들의 인접 리스트 배열
    public static List&amp;lt;Edge&amp;gt;[] nodes;
    // 최단 거리 저장 배열
    public static int[] dist;
    // 노드의 개수, 간선의 개수, 시작 노드
    public static int N, M, start;
    // 무한대 값을 상수로 정의
    private static final int INF = 100_000_000;

    public static void main(String[] args) throws IOException {
        // 입력을 받기 위한 BufferedReader
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken()); // 노드의 수
        M = Integer.parseInt(st.nextToken()); // 간선의 수

        start = Integer.parseInt(br.readLine()); // 시작 노드

        // 최단 거리 배열 초기화
        dist = new int[N + 1];
        Arrays.fill(dist, INF);

        // 노드들의 인접 리스트 배열 초기화
        nodes = new ArrayList[N + 1];
        for (int i = 1; i &amp;lt;= N; i++) {
            nodes[i] = new ArrayList&amp;lt;&amp;gt;();
        }

        // 간선 정보 입력
        for (int i = 0; i &amp;lt; M; i++) {
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken()); // 시작 노드
            int b = Integer.parseInt(st.nextToken()); // 도착 노드
            int c = Integer.parseInt(st.nextToken()); // 비용

            nodes[a].add(new Edge(b, c)); // 간선 리스트에 추가
        }

        // 다익스트라 알고리즘 수행
        dijkstra(start);

        // 결과 출력
        StringBuilder sb = new StringBuilder();
        for (int i = 1; i &amp;lt;= N; i++) {
            if (dist[i] == INF) {
                sb.append(&quot;INF\n&quot;); // 도달할 수 없는 경우 INF 출력
            } else {
                sb.append(dist[i]).append(&quot;\n&quot;); // 최단 거리 출력
            }
        }
        System.out.println(sb.toString());
    }

    // 다익스트라 알고리즘 메서드
    public static void dijkstra(int start) {
        PriorityQueue&amp;lt;Edge&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;();
        // 우선순위 큐에 시작 노드 추가
        pq.add(new Edge(start, 0));
        dist[start] = 0;

        // 다익스트라 알고리즘 수행
        while (!pq.isEmpty()) {
            Edge node = pq.poll(); // 최소 비용의 노드 선택
            int cur = node.end;

            // 이미 처리된 노드는 무시
            if (node.cost &amp;gt; dist[cur]) continue;

            // 현재 노드와 연결된 인접 노드들에 대해 최단 거리 갱신
            for (Edge e : nodes[cur]) {
                if (dist[e.end] &amp;gt; dist[cur] + e.cost) {
                    dist[e.end] = dist[cur] + e.cost; // 최단 거리 갱신
                    pq.add(new Edge(e.end, dist[e.end])); // 갱신된 노드 큐에 추가
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <author>_hanbxx_</author>
      <guid isPermaLink="true">https://hanbxx.tistory.com/80</guid>
      <comments>https://hanbxx.tistory.com/80#entry80comment</comments>
      <pubDate>Tue, 24 Sep 2024 10:49:03 +0900</pubDate>
    </item>
    <item>
      <title>벨만 포드 알고리즘 feat. BOJ11657 타임머신 자바</title>
      <link>https://hanbxx.tistory.com/79</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;벨만포드 알고리즘이란?&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;한 노드에서 다른 노드까지의 최단 거리를 구하는 알고리즘&lt;br /&gt;특징으로는 간선의 가중치가 음수일 때도 최단 거리를 구할 수 있다&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;벨만포드 VS 다익스트라&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[다익스트라 알고리즘]&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매번 방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택하여 한 단계씩 최단 거리를 구해나간다.&lt;/li&gt;
&lt;li&gt;음수 간선이 없다면 최적의 해를 찾을 수 있다.&lt;/li&gt;
&lt;li&gt;시간 복잡도가 빠르다 . 개선된 다익스트라 알고리즘도 존재하는데 이는 우선순위 큐 사용해서 구현할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[벨만포드 알고리즘]&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(정점-1)번의 단게마다 모든 간선을 전부 확인하면서 모든 노드간의 최단 거리를 구한다&lt;/li&gt;
&lt;li&gt;이때 다익스트라와의 차이점은 모든 간선을 확인한다는 것이다. 다익스트라는 방문하지 않은 노드 중에서 최단 거리가 가장 가까운 노드만을 방문한다&lt;/li&gt;
&lt;li&gt;음수 간선이 있어도 최적의 해를 찾을 수 있다&lt;/li&gt;
&lt;li&gt;시간 복잡도가 느리다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이런 차이점으로 인해 모든 간선의 비용이 양수일 때는 다익스트라, 음수 간선이 포함되어 있으면 벨만 포드를 사용하면 되겠다는 결론을 지을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BOJ 11657 - 타임머신&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/11657&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/11657&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1727140238913&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;
import java.lang.*;
import java.io.*;

class Main {
    // 간선을 나타내는 클래스
    public static class Edge {
        int start; // 시작 노드
        int end;   // 도착 노드
        int cost;  // 가중치

        // Edge 클래스의 생성자
        public Edge(int start,int end, int cost) {
            this.start = start;
            this.end = end;
            this.cost = cost;
        }
    }

    // 간선 정보 배열
    public static Edge[] nodes;
    // 노드의 수(N)과 간선의 수(M)
    public static int N, M;

    public static void main(String[] args) throws IOException {
        // 입력을 받기 위한 BufferedReader
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        // 첫 번째 줄에서 N과 M을 입력받음
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken()); // 노드의 수
        M = Integer.parseInt(st.nextToken()); // 간선의 수

        // 최단 거리 배열 (1번 노드부터 시작하므로 N+1 크기)
        long[] dist = new long[N + 1];
        // 간선 배열 초기화
        nodes = new Edge[M + 1];
        // 거리 배열을 최댓값으로 초기화 (도달 불가능한 상태를 의미)
        Arrays.fill(dist, Integer.MAX_VALUE);

        // 간선 정보 입력
        for (int i = 0; i &amp;lt; M; i++) {
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken()); // 시작 노드
            int b = Integer.parseInt(st.nextToken()); // 도착 노드
            int c = Integer.parseInt(st.nextToken()); // 가중치

            nodes[i] = new Edge(a, b, c); // 간선 배열에 추가
        }

        // 시작 노드의 최단 거리를 0으로 설정
        dist[1] = 0;

        // 벨만-포드 알고리즘
        for (int i = 1; i &amp;lt; N; i++) {
            // 모든 간선에 대해 최단 거리 갱신
            for (int j = 0; j &amp;lt; M; j++) {
                Edge e = nodes[j];
                // 시작 노드가 도달 가능하고, 도착 노드까지의 거리가 갱신 가능한 경우
                if (dist[e.start] != Integer.MAX_VALUE &amp;amp;&amp;amp; dist[e.end] &amp;gt; dist[e.start] + e.cost) {
                    dist[e.end] = dist[e.start] + e.cost; // 최단 거리 갱신
                }
            }
        }

        // 음수 사이클 존재 여부 체크
        boolean cycle = false;
        for (int i = 0; i &amp;lt; M; i++) {
            Edge e = nodes[i];
            // 최단 거리가 또 갱신 가능하다면 음수 사이클이 존재함을 의미
            if (dist[e.start] != Integer.MAX_VALUE &amp;amp;&amp;amp; dist[e.end] &amp;gt; dist[e.start] + e.cost) {
                cycle = true;
            }
        }

        // 결과 출력
        if (cycle) {
            System.out.println(-1); // 음수 사이클이 존재하는 경우
        } else {
            // 시작 노드(1)를 제외한 나머지 노드들의 최단 거리 출력
            for (int i = 2; i &amp;lt;= N; i++) {
                if (dist[i] == Integer.MAX_VALUE) {
                    System.out.println(-1); // 도달 불가능한 노드
                } else {
                    System.out.println(dist[i]); // 최단 거리 출력
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코드 설명 추가&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;입력 처리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BufferedReader와 StringTokenizer를 사용하여 입력을 받습니다.&lt;/li&gt;
&lt;li&gt;첫 줄에서 노드의 수(N)와 간선의 수(M)를 입력받습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;간선 정보 입력&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;for 루프를 통해 M개의 간선 정보를 입력받고, nodes 배열에 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;거리 배열 초기화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dist 배열은 노드 수보다 1 크게 설정하여 1번 노드부터 시작할 수 있도록 하였습니다.&lt;/li&gt;
&lt;li&gt;Arrays.fill() 메서드를 사용하여 배열을 Integer.MAX_VALUE로 초기화하여 초기 상태에서 도달할 수 없음을 나타냅니다.&lt;/li&gt;
&lt;li&gt;1번 노드에서 시작하므로 dist[1]을 0으로 설정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;벨만-포드 알고리즘 수행&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;N-1번의 루프를 돌며 모든 간선에 대해 최단 거리를 갱신합니다. 이는 N번째에 최단 거리가 더 갱신된다면 음수 사이클이 존재함을 확인하기 위함입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;음수 사이클 검사&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 번 더 모든 간선을 순회하여 최단 거리가 갱신되는 경우 음수 사이클이 존재함을 의미합니다. 이 경우 cycle 플래그를 true로 설정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과 출력&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;음수 사이클이 존재하는 경우 -1을 출력합니다.&lt;/li&gt;
&lt;li&gt;음수 사이클이 존재하지 않는 경우, 1번 노드를 제외한 나머지 노드의 최단 거리를 출력합니다. 도달할 수 없는 경우 -1을 출력합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>_hanbxx_</author>
      <guid isPermaLink="true">https://hanbxx.tistory.com/79</guid>
      <comments>https://hanbxx.tistory.com/79#entry79comment</comments>
      <pubDate>Tue, 24 Sep 2024 10:11:05 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링에서 사용하는 Util클래스 @Component? Static?</title>
      <link>https://hanbxx.tistory.com/78</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 들어가기에 앞서 @Component의 기본적인 개념에 대해 간략하게 언급해보자면&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;@Component&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 단위로 쓰이며 내부에 종속성이 존재할 때, 즉 내가 만든 클래스를 사용하고 싶을 때 쓸 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 프로젝트에서 &lt;b&gt;Util 클래스&lt;/b&gt;는 Static 메서드를 사용해 구현하는 경우가 많다. 이는 주로 상태가 필요 없고, 특정 기능만 반복적으로 수행하는 단순한 작업에 적합하기 때문이다. 하지만, 내가 현재 참여 중인 프로젝트에서는 @Component를 사용하여 Util 클래스를 정의하고 있어서, 보편적인 Static 메서드와 현재 적용중인 @Component 방식 중 어느 것이 더 적절한지 제대로 알아보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;먼저 Static으로 구성했을 때를 알아보자&lt;br /&gt;&lt;b&gt;Static&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Static메소드는 인자가 동일한 경우 항상 동일한 결과를 리턴해야 한다. 이 규칙을 지킬 수 없으면 Bean으로 만들어야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Static 함수는 모든 객체가 공유하기 위한 함수이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Static 함수는 상태를 가지지 않기 때문에, 외부 의존성을 주입할 수 없다. 그렇기 때문에, 만약 메서드 내에서 다른 클래스의 객체를 사용하거나, 외부 리소스나 의존성이 필요한 작업을 해야 한다면 Static으로 구현하는 것은 적합하지 않다.&amp;nbsp;&lt;br /&gt;예를&amp;nbsp;들어,&amp;nbsp;Spring의&amp;nbsp;의존성&amp;nbsp;주입(DI)이&amp;nbsp;필요한&amp;nbsp;경우,&amp;nbsp;Static&amp;nbsp;메서드로는&amp;nbsp;그&amp;nbsp;기능을&amp;nbsp;활용할&amp;nbsp;수&amp;nbsp;없다.&amp;nbsp;Spring이&amp;nbsp;관리하는&amp;nbsp;Bean&amp;nbsp;객체를&amp;nbsp;사용해야&amp;nbsp;하므로&amp;nbsp;이때는&amp;nbsp;@Component&amp;nbsp;또는&amp;nbsp;@Service와&amp;nbsp;같은&amp;nbsp;애노테이션을&amp;nbsp;사용하여&amp;nbsp;클래스를&amp;nbsp;빈으로&amp;nbsp;등록해야&amp;nbsp;한다.&amp;nbsp;이러한&amp;nbsp;방식으로&amp;nbsp;빈을&amp;nbsp;등록하면,&amp;nbsp;Spring의&amp;nbsp;DI&amp;nbsp;컨테이너를&amp;nbsp;통해&amp;nbsp;다른&amp;nbsp;빈들을&amp;nbsp;주입받아&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에,&amp;nbsp;Static&amp;nbsp;메서드는&amp;nbsp;단순한&amp;nbsp;유틸리티성&amp;nbsp;함수들,&amp;nbsp;예를&amp;nbsp;들어&amp;nbsp;문자열&amp;nbsp;변환,&amp;nbsp;숫자&amp;nbsp;계산,&amp;nbsp;파일&amp;nbsp;경로&amp;nbsp;처리&amp;nbsp;등&amp;nbsp;상태나&amp;nbsp;의존성이&amp;nbsp;전혀&amp;nbsp;필요하지&amp;nbsp;않은&amp;nbsp;경우에&amp;nbsp;적합하다.&amp;nbsp;이런&amp;nbsp;경우&amp;nbsp;Static&amp;nbsp;메서드로&amp;nbsp;구현하면&amp;nbsp;성능적으로도&amp;nbsp;이점이&amp;nbsp;있을&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;모든&amp;nbsp;인스턴스가&amp;nbsp;동일한&amp;nbsp;메서드를&amp;nbsp;공유하기&amp;nbsp;때문에&amp;nbsp;메모리&amp;nbsp;효율성도&amp;nbsp;높아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;@Component&lt;/b&gt;&lt;br /&gt;코드가 복잡해지고, 다른 클래스나 의존성과의 관계가 발생한다면 Static 메서드보다는 Spring 빈을 사용하는 것이 더 유연하고 관리하기 용이하다. 따라서 Static 메서드와 Spring 빈(@Component, @Service 등)은 그 사용 용도가 명확히 나뉘며, 프로젝트의 요구 사항에 따라 적절한 방식을 선택하는 것이 중요하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;결론적으로, Static 메서드는 상태가 없고 외부 의존성 주입이 필요하지 않은 단순한 작업에 적합하며, 반대로 외부 의존성을 필요로 하거나 상태를 관리해야 하는 작업은 @Component나 @Bean을 통해 Spring 빈으로 관리하는 것이 더 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서 현재 &lt;b&gt;FileUtil&lt;/b&gt;을 선언해서 @Component를 달아 사용하고 있는데, 그 이유는 &lt;b&gt;S3 SDK&lt;/b&gt;를 사용해야 하기 때문이다. Static으로 선언할 경우 외부 의존성을 주입받을 수 없기 때문에, 정적인 클래스로 선언하는 것보다는 Spring에서 관리되는 빈으로 등록하여 사용하는 것이 더 나을 것이라고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 상황에 맞게 &lt;b&gt;Util&lt;/b&gt; 클래스를 유연하게 구성할 수 있다는 점을 배울 수 있었다. Util 클래스라도 단순한 작업만 수행하는 경우에는 Static 메서드로, 외부 의존성이나 상태 관리가 필요한 경우에는 Spring 빈으로 관리하는 것이 좋다는 것을 알 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/55805497/spring-dependency-injection-vs-static-classes-for-utilities&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/55805497/spring-dependency-injection-vs-static-classes-for-utilities&lt;/a&gt;&lt;/p&gt;</description>
      <author>_hanbxx_</author>
      <guid isPermaLink="true">https://hanbxx.tistory.com/78</guid>
      <comments>https://hanbxx.tistory.com/78#entry78comment</comments>
      <pubDate>Thu, 22 Aug 2024 12:08:10 +0900</pubDate>
    </item>
    <item>
      <title>Spring 도메인 이벤트 (AbstractAggregateRoot)란? + 적용</title>
      <link>https://hanbxx.tistory.com/77</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;높은 응집도와 낮은 결합도 실현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 개발에서 &lt;b&gt;높은 응집도&lt;/b&gt;와 &lt;b&gt;낮은 결합도&lt;/b&gt;를 실현하는 것은 복잡한 비즈니스 로직을 유지보수하기 쉽게 만드는 핵심 원칙입니다. 높은 응집도는 각 모듈이나 클래스가 하나의 책임에 집중하는 것을 의미하며, 낮은 결합도는 서로 다른 모듈이나 클래스 간의 의존성을 최소화하여 시스템을 확장하고 수정하기 쉽게 만드는 원칙입니다. 이번 프로젝트 개선 작업을 통해 이를 적용해보았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 이벤트 도입 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GDSC 프로젝트에서 준회원 승급 로직을 개선하기 위한 작업을 진행할 때, 기존 코드는 아래와 같이 구현되어 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717657939507&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  public void verifyDiscord(String discordUsername, String nickname) {
        validateStatusUpdatable();
        this.requirement.verifyDiscord();
        this.discordUsername = discordUsername;
        this.nickname = nickname;

        if (isAssociateAvailable()) {
            advanceToAssociate();
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1717419141831&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  /**
     * GUEST -&amp;gt; 준회원으로 승급됩니다.
     * 모든 조건을 충족하면 서버에서 각각의 인증과정에서 자동으로 advanceToAssociate()호출된다
     * 조건 1 : 기본 회원정보 작성
     * 조건 2 : 재학생 인증
     * 조건 3 : 디스코드 인증
     * 조건 4 : Bevy 인증
     */
    public void advanceToAssociate() {
        validateStatusUpdatable();
        validateAssociateAvailable();

        this.role = ASSOCIATE;
        registerEvent(new MemberGrantEvent(discordUsername, nickname));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private boolean isAssociateAvailable() {
    if (!this.requirement.isInfoVerified()) {
        return false;
    }

    if (!this.requirement.isDiscordVerified() || this.discordUsername == null || this.nickname == null) {
        return false;
    }

    if (!this.requirement.isBevyVerified()) {
        return false;
    }

    if (!this.requirement.isUnivVerified() || this.univEmail == null) {
        return false;
    }
    return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 로직은 복잡한 조건을 반복적으로 검증하며, 단일 메서드 안에서 여러 가지 비즈니스 로직과 도메인 로직이 혼합되어 있었습니다. 이로 인해 결합도가 높아지고, 유지보수가 어려워지는 문제가 있었습니다. 검증 로직이 여러 곳에 흩어져 있어 로직을 변경할 때마다 중복된 코드를 수정해야 하는 부담이 컸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 &lt;b&gt;스프링 이벤트 처리&lt;/b&gt;를 도입하여 결합도를 낮추고 관심사를 분리했습니다. 특히 &lt;b&gt;Spring Data JPA&lt;/b&gt;에서 제공하는 AbstractAggregateRoot를 활용하여 도메인 이벤트를 처리함으로써, 비즈니스 로직과 도메인 로직을 명확히 분리할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도메인 이벤트와 관심사 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 verify 함수에서 검증 로직과 준회원 승급 로직이 혼합되어 있었습니다. 이를 해결하기 위해 &lt;b&gt;도메인 이벤트&lt;/b&gt;를 활용하여 관심사를 분리하였습니다. 이제 각 검증 로직은 단일 책임을 가지며, 조건이 충족될 때마다 이벤트가 발생하고, 그 이벤트를 처리하는 별도의 핸들러가 승급 로직을 수행하게 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;b&gt;Spring data jpa의 Domain event는 안티패턴?&lt;/b&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 domain event 방식에도 단점은 존재합니다.&lt;b&gt; Domain event를 트리거하려면 명시적으로 save를 호출&lt;/b&gt;해야 합니다.&amp;nbsp;&lt;br /&gt;기본적으로 @Transactional을 사용하면 jpa의 Dirty Checking 덕분에 repository.save()를 호출하지 않고 코드를 짤 수 있는데,&lt;i&gt; &lt;b&gt;AbstractAggregationRoot&lt;/b&gt;를 통해 registerEvent로 등록해둔 이벤트를 발행하고 싶다면 꼭 반드시 명시적으로 &lt;b&gt;save&lt;/b&gt;를 호출 해야하는 안티 패턴이 존재합니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 이슈의 원인을 깊이 파악하기 위해 디버깅한 결과, save나 delete 메서드를 명시적으로 호출할 때 발생하는 동작을 살펴보면 다음과 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EventPublishingRepositoryProxyPostProcessor에서는 도메인 이벤트를 처리하기 위해 registerEvent로 등록된 이벤트들을 추적하고, 그와 관련된 것들을 스프링에 등록하는 역할을 수행합니다. 즉, 저장(save)이나 삭제(delete) 메서드가 호출되면 이 프로세서가 해당 메서드가 호출되는지를 ReflectionUtil을 활용하여 확인하고, 이를 스프링 이벤트 시스템에 등록하여 처리 과정을 이어나가 이벤트가 발행되게끔 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그럼에도 불구하고..&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA에서 제공하는 registerEvent()와 AbstractAggregateRoot를 사용하는 방식은 공식적인 메커니즘으로, 안티패턴으로 보일 수 있는 부분이 있더라도 충분히 장점을 제공합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 ApplicationEventPublisher로 도메인 이벤트를 직접 발행하는 방식으로 구현할 경우, 모든 이벤트가 제대로 이벤트 큐에 잡히지 않을 가능성에 대비한 추가적인 로직을 구현해야 합니다. 이러한 추가 작업은 비즈니스 로직을 구현하는 데 집중해야 할 리소스를 소모하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, Spring Data JPA의 registerEvent()와 save 메서드를 활용한 도메인 이벤트 방식은 이미 검증된 방법이며, 트랜잭션 내에서 안전하게 이벤트를 처리할 수 있는 구조를 제공합니다. 이로 인해, 비즈니스 로직에 더 집중할 수 있고, 이벤트 처리에 대한 복잡한 예외 처리나 큐 관련 코드를 따로 작성할 필요가 없다는 큰 이점을 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이벤트 처리의 안정성을 확보하면서도, 개발 생산성을 극대화할 수 있다는 판단 때문에 저희는 Domain Event를 활용하여 개발을 진행하였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;적용 과정&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기본설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, abstractAggregationRoot를 적용하기 위해서는 원하는 도메인 엔티티 선언부에 해당 코드를 추가해줘야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1717658626179&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;extends AbstractAggregateRoot&amp;lt;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;코드 개선&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관심사 분리하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 하나의 verify함수안에 (verify하기 + 준회원 승급 여부 판단하기)라는 관심사가 혼합되어 있었다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관심사 분리를 위해 verify를 하면 이벤트를 발행하고 Application Context가 이를 넘겨 받아 Listener가 받아 처리하게 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 가입 조건이 총 4개가 있는데 각각의 조건을 수행할 때마다 함수로 또 검증하는 것이 아닌, 이벤트 처리를 통해 로직이 느슨하게 결합될 수 있도록 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;event handler에서, 즉 비즈니스 로직에서 해당 이벤트가 호출되면 밑에 적어둔 로직만을 처리하게끔MemberAssociateEventListener와 MemberAssociateEventHandler에 선언해 두었습니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;모든 준회원 승급 조건을 만족했는지 체크 -&amp;gt; 만족했다면 승급&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(관심사 분리 -&amp;gt; 준회원 승급 조건이 무엇인지 알 필요도 없고 exception을 던지는 로직 또한 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 예외처리 로직들은 도메인 로직에서 따로 처리해주면 되는 것이다. 오로지 이 Handler는 &quot;승급&quot;에만 관심사를 두게끔 분리를 한것이다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1717658889552&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class MemberAssociateEventHandler {
    private final MemberRepository memberRepository;

    public void advanceToAssociate(MemberAssociateEvent memberAssociateEvent) {
        Member member = memberRepository
                .findById(memberAssociateEvent.memberId())
                .orElseThrow(() -&amp;gt; new CustomException(ErrorCode.MEMBER_NOT_FOUND));
        try {
            member.advanceToAssociate();
        } catch (CustomException e) {
            log.info(&quot;{}&quot;, e.getErrorCode());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1717658942935&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class MemberAssociateEventListener {

    private final MemberAssociateEventHandler memberAssociateEventHandler;

    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT, classes = MemberAssociateEvent.class)
    public void handleMemberAssociateEvent(MemberAssociateEvent event) {
        memberAssociateEventHandler.advanceToAssociate(event);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 전체 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;도메인 객체에서 이벤트 등록:&lt;br /&gt;도메인 객체가 상태 변화를 감지하면 AbstractAggregateRoot의 registerEvent() 메서드를 통해 도메인 이벤트를 등록합니다. 이때, 이벤트는 트랜잭션 내에서 큐에 저장됩니다. 즉, 실제로 이벤트가 발행된 것은 아니며, 트랜잭션이 커밋될 때까지 대기합니다.&lt;/li&gt;
&lt;li&gt;트랜잭션 커밋 시점:&lt;br /&gt;Spring Data JPA는 트랜잭션이 커밋되기 직전에 AbstractAggregateRoot에 등록된 도메인 이벤트를 추적하고, 이 이벤트를 발행합니다. 중요한 점은, 이 이벤트는 트랜잭션이 성공적으로 커밋되기 전까지 발행되지 않습니다. 즉, 트랜잭션이 롤백되면 이벤트는 발행되지 않습니다.&lt;/li&gt;
&lt;li&gt;이벤트 발행:&lt;br /&gt;트랜잭션이 커밋되기 직전, 스프링 이벤트 시스템에 의해 등록된 이벤트가 실제로 발행됩니다. 이때 이벤트는 트랜잭션의 상태에 따라 처리됩니다.&lt;/li&gt;
&lt;li&gt;이벤트 핸들링:&lt;br /&gt;@TransactionalEventListener로 등록된 리스너는 발행된 이벤트를 감지합니다. 이때, 리스너가 언제 이벤트를 처리할지는 @TransactionalEventListener에 설정된 TransactionPhase에 따라 달라집니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;TransactionPhase.BEFORE_COMMIT: 트랜잭션이 커밋되기 직전에 이벤트를 처리합니다.&lt;/li&gt;
&lt;li&gt;TransactionPhase.AFTER_COMMIT: 트랜잭션이 성공적으로 커밋된 후에 이벤트를 처리합니다.&lt;/li&gt;
&lt;li&gt;TransactionPhase.AFTER_ROLLBACK: 트랜잭션이 롤백된 후에 이벤트를 처리합니다.&lt;/li&gt;
&lt;li&gt;TransactionPhase.AFTER_COMPLETION: 트랜잭션이 종료된 후(성공 또는 실패) 이벤트를 처리합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;일반적으로, 비즈니스 로직은 트랜잭션이 성공적으로 커밋된 이후(TransactionPhase.AFTER_COMMIT)에 수행됩니다. 하지만, 제가 구현한 상황처럼 특정 상황에서는 트랜잭션이 커밋되기 전(TransactionPhase.BEFORE_COMMIT)에 로직을 처리해야 할 수도 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1724255629687&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;순서 정리
1단계: 도메인 객체에서 이벤트 등록 (registerEvent()).
2단계: 트랜잭션 커밋 전 대기.
3단계: 트랜잭션 커밋 시점에서 이벤트 발행.
4단계: @TransactionalEventListener가 트랜잭션 단계에 맞춰 이벤트를 감지하고 비즈니스 로직을 처리.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TransactionPhase.BEFORE_COMMIT을 사용한 이유&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;이벤트 핸들링 시점&lt;/b&gt;: 트랜잭션이 커밋되기 직전에 이벤트를 처리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 상태 반영&lt;/b&gt;: 이벤트 핸들러에서 처리된 로직의 결과가 트랜잭션 커밋의 상태에 영향을 미칠 수 있습니다. 즉, 이벤트 핸들러에서 발생하는 예외가 트랜잭션 롤백을 유발할 수 있으며, 상태 변경이 트랜잭션 커밋의 일부로 간주됩니다. 특히 이벤트를 통해 객체의 상태가 변화가 되는 로직에서는 이벤트 핸들러에서 발생하는 예외를 롤백시켜야 하기때문에 BEFORE_COMMIT을 사용하였습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;: 상태 변화가 트랜잭션의 일부분으로 간주되어 데이터 일관성이 유지됩니다. 트랜잭션이 커밋되기 전 상태를 확정할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적용 이유&lt;/b&gt;: 회원 승급 과정에서 승급 여부를 결정해야 하고, 이 결과가 트랜잭션 커밋에 영향을 미쳐야 하기 때문입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717659002377&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
public class MemberIntegrationTest extends IntegrationTest {
    @Autowired
    private MemberAssociateEventHandler memberAssociateEventHandler;

    @Test
    void 준회원_승급조건_만족됐으면_MemberRole은_ASSOCIATE이다() {
        // given
        Member member = createMember();

        // when
        memberAssociateEventHandler.advanceToAssociate(new MemberAssociateEvent(member.getId()));
        member = memberRepository.save(member);

        // then
        assertThat(member.getRole()).isEqualTo(ASSOCIATE);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>_hanbxx_</author>
      <guid isPermaLink="true">https://hanbxx.tistory.com/77</guid>
      <comments>https://hanbxx.tistory.com/77#entry77comment</comments>
      <pubDate>Wed, 21 Aug 2024 17:37:04 +0900</pubDate>
    </item>
    <item>
      <title>[스프링] 쿼리 최적화로 504 GatewayTimeOut의 근본적인 문제를 해결해보자 feat: jpa, index</title>
      <link>https://hanbxx.tistory.com/76</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 파일 관리 시스템의 일환으로 Presigned URL을 대량으로 생성하는 API를 개발했습니다. 로컬 환경에서는 100개 이상의 URL도 1초 내로 생성할 수 있었습니다. 그러나 AWS에 배포된 실제 서버에서는 30개 이상의 URL을 요청하면 504 Timeout 오류가 발생했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인 파악&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 자세히 분석한 결과, 문제의 원인을 파악할 수 있었습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;배포 서버는 AWS Elastic Load Balancer(ELB)를 이용하고 있는데, 이 ELB는 기본적으로 60초 후에 Timeout을 발생시킵니다.Timeout 발생 후 약 1분이 지나면 요청한 작업이 백그라운드에서 완료되었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;하지만 이 504 에러코드가 뜨는 근본적인 원인을 찾아야 했습니다. 그래서 기존의 코드를 분석해 보니 쿼리 최적화가 하나도 되어 있지 않은 비효율적인 코드로 구현되어 있었습니다. 그리고 로컬에서 100개를 요청해도 됐던 이유는, 실 서버만큼 100만개가 넘는 데이터가 로컬 디비에 존재하지 않고 있었기 때문입니다. 로컬 디비와 서버 디비 환경을 동일하게 해놓고 문제점을 파악해보니, 쿼리에 대한 개선책이 확실하게 필요한 상황이었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 문제 상황 파악&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 코드를 보여드리겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1723525507215&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Transactional
    public List&amp;lt;PresignedUrlResponse&amp;gt; createPresignedUrls(PresignedUrlRequest presignedUrlRequest) {
        List&amp;lt;PresignedUrlResponse&amp;gt; presignedUrlResponses = new ArrayList&amp;lt;&amp;gt;();

        for (PresignedUrlDto request : presignedUrlRequest.presignedUrlDto()) {
            URI presignedUrl = createPresignedUrl(request);
            File file = getFile(request);
            PresignedUrlResponse response = PresignedUrlResponse.from(file, presignedUrl);
            presignedUrlResponses.add(response);
        }

        log.info(
                &quot;Presigned Url 생성 완료: ids={}&quot;,
                presignedUrlResponses.stream().map(PresignedUrlResponse::fileId).toList());

        return presignedUrlResponses;
    }

    /**
     * Presigned Url을 발급한다.
     */
    private URI createPresignedUrl(PresignedUrlDto request) {
        return fileUtil.getPresignedUrl(request.projectId(), request.bookId(), request.name(), request.extension());
    }

    /**
     * 이미 존재하는 파일에 대한 검증을 먼저 진행하고
     * 존재한다면 updateFile 후에 해당 파일이 반환된다.
     * 새로운 파일이라면 saveNewFile 후에 해당 파일이 반환된다
     */
    private File getFile(PresignedUrlDto request) {
        Optional&amp;lt;File&amp;gt; file = fileRepository.findByProjectIdAndBookIdAndNameAndExtension(
                request.projectId(), request.bookId(), request.name(), request.extension());
        return file.map(value -&amp;gt; updateExistingFile(value, request)).orElseGet(() -&amp;gt; saveNewFile(request));
    }

    private File updateExistingFile(File file, PresignedUrlDto request) {
        if (isFileTypeDocument(file.getExtension())) {
            file.updateSize(request.size());
            return file;
        }

        ImageFile imageFile = (ImageFile) file;
        imageFile.update(request.size(), request.resolution(), request.width(), request.height());
        return file;
    }

    private File saveNewFile(PresignedUrlDto request) {
        if (isFileTypeDocument(request.extension())) {
            return saveDocumentFile(
                    request.projectId(), request.bookId(), request.name(), request.size(), request.extension());
        }

        return saveImageFile(
                request.projectId(),
                request.bookId(),
                request.name(),
                request.size(),
                request.extension(),
                request.resolution(),
                request.width(),
                request.height());
    }

    private File saveImageFile(
            Long projectId,
            Long bookId,
            String name,
            Long size,
            FileExtension extension,
            Double resolution,
            Long width,
            Long height) {
        ImageFile imageFile = ImageFile.create(
                projectId,
                bookId,
                fileUtil.getDomain(),
                fileUtil.getBucket(),
                name,
                size,
                extension,
                resolution,
                width,
                height);
        return fileRepository.save(imageFile);
    }

    private File saveDocumentFile(Long projectId, Long bookId, String name, Long size, FileExtension extension) {
        DocumentFile documentFile = DocumentFile.create(
                projectId, bookId, fileUtil.getDomain(), fileUtil.getBucket(), name, size, extension);
        return fileRepository.save(documentFile);
    }

    /**
     * 파일 타입을 알아내는 함수
     * 문서는 true반환, 이미지는 false반환
     */
    private boolean isFileTypeDocument(FileExtension extension) {
        return extension == XML || extension == TOC || extension == PDF;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 분석을 통한 문제점 파악&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드를 분석한 결과, 데이터베이스 접근하는 코드에서 비효율적인 부분을 발견했습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터베이스 접근의 비효율성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723594842898&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private File getFile(PresignedUrlDto request) {
    Optional&amp;lt;File&amp;gt; file = fileRepository.findByProjectIdAndBookIdAndNameAndExtension(
            request.projectId(), request.bookId(), request.name(), request.extension());
    return file.map(value -&amp;gt; updateExistingFile(value, request)).orElseGet(() -&amp;gt; saveNewFile(request));
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 메서드는 매 반복마다 데이터베이스를 개별적으로 조회하고 있었습니다. 대량 요청 시 심각한 성능 저하를 초래하는 가장 큰 원인 중 하나였습니다.&lt;/li&gt;
&lt;li&gt;이로 인해 발생한 문제점들은 바로
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;i&gt;데이터베이스 연결 오버헤드: 각 쿼리마다 새로운 데이터베이스 연결을 맺고 끊는 과정이 반복됩니다. 입출력(I/O) 오버헤드가 증가하고 있었습니다.&lt;/i&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;네트워크&amp;nbsp;지연:&amp;nbsp;각&amp;nbsp;쿼리마다&amp;nbsp;네트워크&amp;nbsp;왕복&amp;nbsp;시간이&amp;nbsp;소요되기&amp;nbsp;때문에,&amp;nbsp;대량의&amp;nbsp;쿼리가&amp;nbsp;실행될&amp;nbsp;경우&amp;nbsp;네트워크&amp;nbsp;왕복&amp;nbsp;시간이&amp;nbsp;길어지면서&amp;nbsp;응답&amp;nbsp;시간이&amp;nbsp;1분을&amp;nbsp;넘게&amp;nbsp;되었습니다.&amp;nbsp;그&amp;nbsp;결과,&amp;nbsp;AWS&amp;nbsp;ELB에서&amp;nbsp;타임아웃이&amp;nbsp;발생하여&amp;nbsp;결국&amp;nbsp;504&amp;nbsp;오류가&amp;nbsp;반환된&amp;nbsp;것입니다.&lt;/b&gt;&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;부하 테스트 : 문제를 대해 더 자세하게 파헤쳐 보기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;이미 데이터베이스에 존재하는 동일한 파일 데이터 500개에 대한 Presigned Url을 반환&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1907&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3PWE7/btsI3Vty0AS/RFGKXVf4t0t1l1odk90Y8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3PWE7/btsI3Vty0AS/RFGKXVf4t0t1l1odk90Y8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3PWE7/btsI3Vty0AS/RFGKXVf4t0t1l1odk90Y8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3PWE7%2FbtsI3Vty0AS%2FRFGKXVf4t0t1l1odk90Y8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1907&quot; height=&quot;150&quot; data-origin-width=&quot;1907&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 약 2분 49초&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 테스트만 해도 충분히 최적화가 필요한 것을 알 수 있지만, 실제로 프론트와 통신할 때 생기는 다른 상황을 추가로 가정해서 부하테스트를 더 진행해보았습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이미 데이터베이스에 존재하는 각기 다른 파일 데이터 500개에 대한 Presigned Url을 반환 -&amp;gt; 약 5분&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;데이터베이스에 존재하지 않는 각기 다른 파일 데이터 500개에 대한 Presigned Url 반환 -&amp;gt; 약 3분&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법 1: 인덱스 - JPA에서 인덱스 설정하기&lt;/h2&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1. 스프링 data jpa를 이용하여 테이블 정의할 때 사용하기&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스를 걸어버리면 최소 0.6초 가까이 나옵니다 .. 3분에서 0.6초는 엄청나게 줄어든 수치이긴 합니다만...&lt;/p&gt;
&lt;pre id=&quot;code_1723595169983&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;file&quot;, indexes = {
    @Index(name = &quot;idx_file_attributes&quot;, columnList = &quot;projectId,bookId,name,extension&quot;)
})
public class File {
    // ... 필드 정의
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 여전히 부족했던 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 적용을 하려면 다음과 같은 문제점들도 고려해야 했습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;쓰기 성능 저하&lt;/b&gt;: 새로운 파일 정보를 자주 추가하는 우리 서비스 특성상, 인덱스로 인한 쓰기 성능 저하는 무시할 수 없었습니다. 인덱스를 적용한다 해도 데이터베이스 I/O 호출량이 엄청나게 줄어들지 않기 때문입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;저장 공간 증가&lt;/b&gt;: 대용량 데이터를 다루는 우리 시스템에서 인덱스로 인한 추가 저장 공간은 상당한 비용 증가를 의미했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복잡한 쿼리에 대한 한계&lt;/b&gt;: 단순 인덱스만으로는 우리 서비스의 복잡한 조회 패턴을 모두 커버하기 어려웠습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 인덱스 적용을 하더라도 현재 수용 가능한 범위를 넘어서면 또 문제가 발생할 수 밖에 없다는 것을 깨달았습니다. 데이터를 많이 사용하는 회사 프로젝트인 만큼 인덱스 적용으로 발생될 메모리도 고려하지 않을 수 없었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;(진) 해결 방법 2: 쿼리 최적화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인덱스 적용은 임시방편임을 깨달은 후, 쿼리 자체를 최적화하는 방향으로 전략을 수정했습니다. 기존 코드의 비효율성을 개선하여 다음과 같이 변경했습니다:&lt;/p&gt;
&lt;pre id=&quot;code_1723595694833&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public List&amp;lt;PresignedUrlResponse&amp;gt; createPresignedUrls(PresignedUrlRequest presignedUrlRequest) {
    List&amp;lt;PresignedUrlResponse&amp;gt; presignedUrlResponses = new ArrayList&amp;lt;&amp;gt;();

    List&amp;lt;String&amp;gt; fileNames = presignedUrlRequest.presignedUrlDto().stream()
            .map(PresignedUrlDto::name)
            .toList();

    List&amp;lt;File&amp;gt; fileList = fileRepository.findByProjectIdAndBookIdAndNameIn(
            presignedUrlRequest.projectId(), presignedUrlRequest.bookId(), fileNames);

    for (PresignedUrlDto request : presignedUrlRequest.presignedUrlDto()) {
        URI presignedUrl =
                createPresignedUrl(request, presignedUrlRequest.projectId(), presignedUrlRequest.bookId());
        File file =
                saveOrUpdateFile(request, fileList, presignedUrlRequest.projectId(), presignedUrlRequest.bookId());
        PresignedUrlResponse response = PresignedUrlResponse.from(file, presignedUrl);
        presignedUrlResponses.add(response);
    }

    log.info(
            &quot;Presigned Url 생성 완료: ids={}&quot;,
            presignedUrlResponses.stream().map(PresignedUrlResponse::fileId).toList());

    return presignedUrlResponses;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;1. 배치 조회 도입:&lt;/h3&gt;
&lt;pre id=&quot;code_1723595714831&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;File&amp;gt; fileList = fileRepository.findByProjectIdAndBookIdAndNameIn(
           presignedUrlRequest.projectId(), presignedUrlRequest.bookId(), fileNames);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;개별 파일마다 데이터베이스를 조회하던 방식에서 벗어나, 한 번의 쿼리로 필요한 모든 파일 정보를 가져오도록 변경했습니다. 이 변경으로 데이터베이스 호출 횟수를 대폭 줄였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;1. &lt;b&gt;배치 조회 (Batch Retrieval): &lt;/b&gt;여러 파일 정보를 개별적으로 조회하는 대신, 한 번의 쿼리로 모든 파일 정보를 가져오는 방식입니다. 이는 SQL IN 절을 사용해 fileNames에 포함된 모든 파일을 한꺼번에 가져오는 방식으로, 데이터베이스의 효율성을 극대화할 수 있었습니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;2. &lt;b&gt;데이터베이스 호출 감소:&lt;/b&gt; 데이터베이스에 대한 쿼리 호출 횟수가 한 번으로 줄어듭니다. 결과적으로 데이터베이스 부하를 줄이며 성능을 향상시킬 수 있었습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;2. 메모리 내 처리 최적화:&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1723595783511&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   private File saveOrUpdateFile(PresignedUrlDto request, List&amp;lt;File&amp;gt; fileList, Long projectId, Long bookId) {
       Optional&amp;lt;File&amp;gt; fileOptional = fileList.stream()
               .filter(file -&amp;gt; file.getName().equals(request.name()))
               .filter(file -&amp;gt; file.getExtension().equals(request.extension()))
               .findFirst();

       return fileOptional
               .map(file -&amp;gt; updateExistingFile(file, request))
               .orElseGet(() -&amp;gt; saveNewFile(request, projectId, bookId));
   }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;데이터베이스에서 가져온 파일 목록을 메모리에 저장하고, 이를 활용하여 파일 존재 여부를 확인하고 업데이트하는 로직을 구현했습니다. &lt;/span&gt;이를 통해 불필요한 데이터베이스 조회를 줄이고, 처리 속도를 향상시켰습니다.&lt;br /&gt;&lt;br /&gt;3. 코드 구조 개선:&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;메서드를&amp;nbsp;더&amp;nbsp;작고&amp;nbsp;명확한&amp;nbsp;책임을&amp;nbsp;가진&amp;nbsp;단위로&amp;nbsp;분리하여,&amp;nbsp;코드의&amp;nbsp;가독성과&amp;nbsp;유지보수성을&amp;nbsp;향상시켰습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;성능 개선 결과&lt;/b&gt;&lt;br /&gt;이러한 최적화 적용 후, 다음과 같은 성능 개선을 달성했습니다:&lt;br /&gt;: &lt;b&gt;평균 응답 시간: 3분 &amp;rarr; 600ms (99% 감소)&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이번 쿼리 최적화 과정에서 몇 가지 중요한 교훈을 얻었습니다:&lt;br /&gt;&lt;br /&gt;&lt;b&gt;1. 데이터 접근 패턴 분석의 중요성:&lt;/b&gt; 단순히 기술적 최적화 기법을 무작정 적용하는 것보다, 서비스의 데이터 사용 패턴을 정확하게 이해하는 것이 더욱 중요하다는 것을 배웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예를 들어, 인덱스를 무작정 추가하는 것만으로는 성능 개선을 보장할 수 없었습니다. 특히 추후에 발생할 메모리 문제를 감당하지 못했을 것 같습니다. 오히려, 데이터베이스 접근 패턴을 면밀히 분석하고, 실제 성능 저하가 발생하는 부분을 정확히 파악한 후, 그에 맞춘 최적화 전략을 수립하는 것이 성능 향상에 훨씬 더 효과적이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;2. 벌크 연산의 효과:&lt;/b&gt; 개별 처리보다 벌크 연산을 활용함으로써 드라마틱한 성능 향상을 얻을 수 있었습니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <author>_hanbxx_</author>
      <guid isPermaLink="true">https://hanbxx.tistory.com/76</guid>
      <comments>https://hanbxx.tistory.com/76#entry76comment</comments>
      <pubDate>Wed, 14 Aug 2024 09:53:34 +0900</pubDate>
    </item>
    <item>
      <title>Spring에서 API 연동 도구 선택(feat: WebClient, RestClient)</title>
      <link>https://hanbxx.tistory.com/75</link>
      <description>&lt;h3&gt;&lt;strong&gt;외부 API 연동, 어떻게 해야 최선일까?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;서버 개발자라면 외부 API 연동은 이제 기본입니다. 저도 공공데이터, 영화진흥위원회, 심지어 카카오 로그인까지 다양한 API 연동 작업을 해왔어요. 처음에는 &amp;quot;이렇게 하면 되는구나~&amp;quot; 하고 OkHttp로 시작해서 Feign까지 써봤죠.&lt;/p&gt;
&lt;p&gt;그런데 문제는, 그냥 돌아가게만 만들었지, &lt;strong&gt;왜 이렇게 동작하는지&lt;/strong&gt;, &lt;strong&gt;어떤 도구가 더 좋은지&lt;/strong&gt; 제대로 이해하지 못했다는 겁니다. 시간이 없기도 했고, 솔직히 귀찮기도 했고요...  &lt;/p&gt;
&lt;p&gt;그러다 문득 이런 생각이 들었어요. &lt;strong&gt;&amp;quot;만약 면접에서 &amp;#39;이 상황에서는 OkHttp 대신 왜 Feign이나 WebClient, RestClient를 써야 하나요?&amp;#39;라고 질문을 받으면 뭐라고 대답하지?&amp;quot;&lt;/strong&gt; 아차 싶었죠. 그래서 이번에 제대로 공부해보기로 했습니다. 마침 이번 프로젝트에서 Spring Quartz를 이용해 특정 시간대에 적지 않은 데이터를 호출해 서버에서 가공하고 저장까지 해야 하는 요구사항이 있었거든요. 이 상황에서 &lt;strong&gt;최선의 선택&lt;/strong&gt;은 무엇인지, 그리고 그 선택에 대한 &lt;strong&gt;타당한 이유&lt;/strong&gt;까지 완벽하게 이해하고 넘어가고 싶었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;1. 외부 API 연결 도구 선정&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;먼저, 초반에는 위에서 언급한 요구사항에 맞는 도구로 &lt;strong&gt;WebClient&lt;/strong&gt;를 선정했습니다. 단순한 HttpUrlConnection이나 OkHttp와 비교해, &lt;strong&gt;왜 굳이 WebClient를 선택했는지&lt;/strong&gt;, 어떤 장점이 있는지 설명해볼게요.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;1.1 HttpURLConnection/URLConnection&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기본적인 HTTP 통신&lt;/strong&gt;: 자바에서 제공하는 기본 API로, 순수 자바로 HTTP 통신을 지원합니다. 데이터의 타입이나 길이에 거의 제한이 없죠.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;동기적 통신&lt;/strong&gt;: 오래된 자바 버전에서 사용하는 이 클래스는 동기적 통신을 기본으로 합니다. 요청을 보내고 응답을 기다리는 동안 스레드가 대기 상태에 머물러 성능에 큰 영향을 미칠 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;불편함&lt;/strong&gt;: 기본적인 요청과 응답 기능을 제공하지만, 추가적인 기능들은 사용자가 직접 구현해야 하는 불편함이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;1.2 OkHttpClient&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;간편한 사용성&lt;/strong&gt;: 단순하고 직관적인 API를 제공해 사용하기 쉽습니다. 응답 캐싱을 지원해 중복 요청을 방지하고 네트워크 트래픽을 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;안정성 문제&lt;/strong&gt;: 최근에 출시된 라이브러리라 안정성이 완벽하게 검증되지 않았습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;메모리 문제&lt;/strong&gt;: OkHttp의 프로세스 메모리가 크고, 간혹 Out of Memory (OOM) 에러가 발생하는 경우가 많습니다. 특히 OkHttp의 Direct Buffer Pool은 GC(Garbage Collect)를 통해 회수가 안 되는 문제가 있어, 장기간 사용 시 메모리 누수가 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;1.3 WebClient&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;비동기/논블로킹 처리&lt;/strong&gt;: 외부 API 호출을 비동기 및 논블로킹 방식으로 처리할 수 있습니다. 이는 I/O 작업이 많거나 네트워크 통신이 빈번한 애플리케이션에서 특히 유용합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;리액티브 프로그래밍&lt;/strong&gt;: 데이터 스트림을 효과적으로 처리할 수 있어, 높은 처리량과 확장성을 확보할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 학습 장벽이 높습니다. 새로운 개념인 리액티브 프로그래밍을 이해하고 적용하는 데 시간이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;결론&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;이러한 이유로, 주기적으로 대량의 외부 API 데이터를 처리하고 이를 데이터베이스에 저장해야 하는 &lt;strong&gt;복잡한 워크플로우&lt;/strong&gt;에서 &lt;strong&gt;WebClient&lt;/strong&gt;를 사용하여 비동기적으로 처리하는 것이 최적의 선택이라고 판단했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;2. 실질적인 통신 과정에 대한 고민&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;WebClient를 사용하여 비동기 또는 논블로킹 방식으로 I/O를 처리하는 것에 대한 고민은 &lt;strong&gt;데이터베이스의 트랜잭션&lt;/strong&gt;과 어떻게 상호작용하는지에 대한 중요한 질문으로 이어집니다. 비동기 처리 방식이 데이터베이스 트랜잭션에 미치는 영향과 이러한 방식이 정말 최선의 선택인지에 대해 살펴보겠습니다.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;2.1 비동기와 논블로킹 처리의 데이터베이스 트랜잭션과의 상호작용&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;비동기 I/O의 트랜잭션 관리&lt;/strong&gt;:&lt;br&gt;비동기 I/O를 사용할 때, 네트워크 요청과 응답은 비동기적으로 처리되지만, &lt;strong&gt;데이터베이스 트랜잭션&lt;/strong&gt;은 여전히 동기적으로 관리됩니다. 즉, 비동기적으로 외부 API에서 데이터를 가져와도, 데이터베이스에 저장하거나 트랜잭션을 처리하는 과정은 여전히 동기적으로 이루어집니다. 이로 인해 비동기 I/O와 데이터베이스 트랜잭션 간의 &lt;strong&gt;상호작용&lt;/strong&gt;에서 신중해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;트랜잭션 경계와 일관성&lt;/strong&gt;:&lt;br&gt;비동기 처리 중 데이터베이스 트랜잭션을 관리할 때는 &lt;strong&gt;트랜잭션 경계&lt;/strong&gt;를 명확히 설정하는 것이 중요합니다. 비동기 작업이 완료되기 전까지 트랜잭션을 유지해야 하며, 이 과정에서 &lt;strong&gt;트랜잭션의 일관성&lt;/strong&gt;을 보장해야 합니다. 예를 들어, 비동기 API 호출 후 결과를 데이터베이스에 저장하는 경우, 데이터베이스 트랜잭션이 비동기 작업의 완료를 기다리는 동안 유지되어야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;데이터베이스 연결과 스레드 관리&lt;/strong&gt;:&lt;br&gt;비동기 I/O가 데이터베이스 트랜잭션과 상호작용할 때는 &lt;strong&gt;스레드 관리&lt;/strong&gt;와 &lt;strong&gt;데이터베이스 연결 관리&lt;/strong&gt;가 중요합니다. 비동기 작업으로 인해 여러 스레드가 데이터베이스에 동시에 접근할 수 있으므로, &lt;strong&gt;데이터베이스 연결 풀&lt;/strong&gt;과 &lt;strong&gt;스레드 풀&lt;/strong&gt;을 적절하게 관리해야 합니다. 특히, 스레드 풀이 과도하게 많거나 적절히 설정되지 않으면 &lt;strong&gt;Context Switching Overhead&lt;/strong&gt;와 같은 성능 문제를 초래할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;비동기 처리의 복잡성&lt;/strong&gt;:&lt;br&gt;비동기 처리는 동시성 관리와 스레드 간의 상호작용이 복잡하기 때문에, 데이터베이스 트랜잭션을 관리하는 데 추가적인 &lt;strong&gt;복잡성&lt;/strong&gt;이 생길 수 있습니다. 이러한 복잡성은 개발과 유지보수에서 추가적인 부담이 될 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;2.2 다른 방법... RestClient?&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;이런 주의점에 대해 알아본 후 저는 WebClient를 사용하는 것이 과연 옳은 선택일까에 대한 고민을 했습니다. 비동기식으로 API 호출하는 것과 동기식으로 API 호출하는 것에 드라마틱한 차이점이 없는데 굳이 Webclient를 사용하기 위해서 새로운 R2DBC나 NoSQL과 같은 조금은 러닝 커브가 있는 것들을 도입하는 것이 최선일까 의문을 품었습니다.&lt;br&gt;비동기 처리를 사용하지 않는다면 WebClient 대신 어떤 도구를 사용할 수 있을까요? 이런 고민 끝에 최근에 등장한 &lt;strong&gt;RestClient&lt;/strong&gt;를 발견했습니다. &lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;RestClient&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spring 6.1.2 버전부터 등장&lt;/strong&gt;: WebFlux 의존성 없이 사용할 수 있는 도구입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;동기식으로 동작&lt;/strong&gt;: WebClient와 유사하지만 동기식으로 동작합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용 편리성&lt;/strong&gt;: HttpInterface라는 유용한 도구를 WebClient와 함께 사용해야 했지만, RestClient가 등장하면서 이러한 번거로움을 해소했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;결론&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;이 RestClient를 보고, 기존에 결정한 WebClient를 &lt;strong&gt;대체&lt;/strong&gt;하기로 했습니다. 비동기적 통신의 리스크와 굳이 리액티브 프로그래밍 환경이 아닌 이상 RestClient를 두고 WebClient를 사용할 이유가 없다고 판단했기 때문입니다. 저는 결론적으로 RestClient와 HttpInterface를 활용해서 외부 API를 호출하여 데이터를 가져오는 작업을 진행하기로 결정했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;3. 외부 API와 연동, 그 이후&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;외부 API와 통신이 된다고 해서 끝이 아닙니다. 이제 RestClient로 단순히 외부 데이터를 가져와 작동시키는 것을 넘어, &lt;strong&gt;더 효과적으로 처리할 방법&lt;/strong&gt;에 대해 고민해보려고 합니다. 예를 들어, &lt;strong&gt;Rate Limit&lt;/strong&gt;을 적용하거나, &lt;strong&gt;타임아웃 설정&lt;/strong&gt;, &lt;strong&gt;캐싱 처리&lt;/strong&gt; 등 무궁무진한 효율적인 개선 방법들을 찾아볼 예정입니다. 앞으로 더 많은 고민과 개선을 통해 최적의 API 연동 방식을 찾아가겠습니다. &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;이렇게 정리된 글을 통해 비동기 I/O와 트랜잭션 관리의 복잡성, 그리고 각 도구의 장단점을 이해하고, 최적의 선택을 하는 데 도움이 되기를 바랍니다!&lt;/p&gt;</description>
      <author>_hanbxx_</author>
      <guid isPermaLink="true">https://hanbxx.tistory.com/75</guid>
      <comments>https://hanbxx.tistory.com/75#entry75comment</comments>
      <pubDate>Sun, 11 Aug 2024 01:07:23 +0900</pubDate>
    </item>
    <item>
      <title>[스프링 DB 접근 기술 1편] 4. 스프링과 문제 해결</title>
      <link>https://hanbxx.tistory.com/74</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;애플리케이션&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프레젠테이션 계층&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 요청과 응답&lt;/li&gt;
&lt;li&gt;사용자 요청을 검증&lt;/li&gt;
&lt;li&gt;주 사용 기술 : 서블릿과 HTTP와 같은 웹 기술, 스프링 MVC&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 계층&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비즈니스 로직을 담당&lt;/li&gt;
&lt;li&gt;주 사용 기술 : 가급적 특정 기술에 의존하지않고 순수 자바 코드로 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 접근 계층&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 데이터 베이스에 접근하는 코드&lt;/li&gt;
&lt;li&gt;주 사용 기술 : JDBC, JPA, File, Redis, Mongo&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 가장 중요한 곳은 바로 핵심 비즈니스 로직이 들어있는 서비스 계층&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 흘러서 UI와 관련된 부분이 변하고 데이터 저장 기술을 다른 기술로 변경해도 비즈니스 로직은 최대한 변경없이 유지되어야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 계층은 가급적 비즈니스 로직만 구현하고 특정 구현 기술에 직접 의존해서는 안된다. 이렇게 하면 향후 구현 기술이 변경될 떄 변경의 영향 범위를 최소화 할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 강의의 코드&lt;/p&gt;
&lt;pre id=&quot;code_1716409148321&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class MemberServiceV1 {
private final MemberRepositoryV1 memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws 
SQLException {
Member fromMember = memberRepository.findById(fromId); 
Member toMember = memberRepository.findById(toId);
        memberRepository.update(fromId, fromMember.getMoney() - money); 
        memberRepository.update(toId, toMember.getMoney() + money); 
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순수한 비즈니스 로직만 존재&lt;/li&gt;
&lt;li&gt;특정 기술과 관련된 코드가 거의 없어서 코드가 깔끔하고 유지보수 하기 쉽다&lt;/li&gt;
&lt;li&gt;문제점은 남아 있다. SQLException&amp;nbsp;이라는&amp;nbsp;JDBC&amp;nbsp;기술에&amp;nbsp;의존한다는&amp;nbsp;점이다. &lt;br /&gt;이&amp;nbsp;부분은&amp;nbsp;memberRepository&amp;nbsp;에서&amp;nbsp;올라오는&amp;nbsp;예외이기&amp;nbsp;때문에&amp;nbsp;memberRepository&amp;nbsp;에서&amp;nbsp;해결해야&amp;nbsp;한다.&amp;nbsp; &lt;br /&gt;이&amp;nbsp;부분은&amp;nbsp;뒤에서&amp;nbsp;예외를&amp;nbsp;다룰&amp;nbsp;때&amp;nbsp;알아보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1716409284681&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor 
public class MemberServiceV2 {
private final DataSource dataSource;
private final MemberRepositoryV2 memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws 
SQLException {
Connection con = dataSource.getConnection(); 
try {
            con.setAutoCommit(false); //트랜잭션 시작
//비즈니스 로직
bizLogic(con, fromId, toId, money);
            con.commit(); //성공시 커밋
        } catch (Exception e) {
            con.rollback(); //실패시 롤백
throw new IllegalStateException(e);
        } finally {
release(con);
        } 
    }
private void bizLogic(Connection con, String fromId, String toId, int money) 
throws SQLException {
Member fromMember = memberRepository.findById(con, fromId); 
Member toMember = memberRepository.findById(con, toId);
        memberRepository.update(con, fromId, fromMember.getMoney() - money); 
        memberRepository.update(con, toId, toMember.getMoney() + money); 
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작하는 것이 좋다&lt;/li&gt;
&lt;li&gt;문제 : 트랜잭션을 사용하기 위해 javax.sql.DataSource&amp;nbsp;,&amp;nbsp;java.sql.Connection&amp;nbsp;,&amp;nbsp; &lt;br /&gt;java.sql.SQLException&amp;nbsp;같은&amp;nbsp;JDBC&amp;nbsp;기술에&amp;nbsp;의존해야&amp;nbsp;한다는&amp;nbsp;점이다.&lt;/li&gt;
&lt;li&gt;트랜잭션을&amp;nbsp;사용하기&amp;nbsp;위해&amp;nbsp;JDBC&amp;nbsp;기술에&amp;nbsp;의존한다.&amp;nbsp;결과적으로&amp;nbsp;비즈니스&amp;nbsp;로직보다&amp;nbsp;JDBC를&amp;nbsp;사용해서&amp;nbsp;트랜잭션&amp;nbsp; &lt;br /&gt;을 처리하는 코드가 더 많다.&lt;/li&gt;
&lt;li&gt;향후&amp;nbsp;JDBC에서&amp;nbsp;JPA&amp;nbsp;같은&amp;nbsp;다른&amp;nbsp;기술로&amp;nbsp;바꾸어&amp;nbsp;사용하게&amp;nbsp;되면&amp;nbsp;서비스&amp;nbsp;코드도&amp;nbsp;모두&amp;nbsp;함께&amp;nbsp;변경해야&amp;nbsp;한다.&amp;nbsp;(JPA는&amp;nbsp; &lt;br /&gt;트랜잭션을&amp;nbsp;사용하는&amp;nbsp;코드가&amp;nbsp;JDBC와&amp;nbsp;다르다.)&lt;/li&gt;
&lt;li&gt;핵심 비즈니스 로직과 JDBC 기술이 섞여 있어 유지보수 하기 어렵다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제 정리&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션 문제&lt;/li&gt;
&lt;li&gt;예외 누수 문제
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터&amp;nbsp;접근&amp;nbsp;계층의&amp;nbsp;JDBC&amp;nbsp;구현&amp;nbsp;기술&amp;nbsp;예외가&amp;nbsp;서비스&amp;nbsp;계층으로&amp;nbsp;전파된다.&lt;/li&gt;
&lt;li&gt;SQLException&amp;nbsp;은&amp;nbsp;체크&amp;nbsp;예외이기&amp;nbsp;때문에&amp;nbsp;데이터&amp;nbsp;접근&amp;nbsp;계층을&amp;nbsp;호출한&amp;nbsp;서비스&amp;nbsp;계층에서&amp;nbsp;해당&amp;nbsp;예외를&amp;nbsp;잡아서&amp;nbsp;처리&amp;nbsp; &lt;br /&gt;하거나&amp;nbsp;명시적으로&amp;nbsp;throws&amp;nbsp;를&amp;nbsp;통해서&amp;nbsp;다시&amp;nbsp;밖으로&amp;nbsp;던져야한다&lt;/li&gt;
&lt;li&gt;SQLException&amp;nbsp;은&amp;nbsp;JDBC&amp;nbsp;전용&amp;nbsp;기술이다.&amp;nbsp;향후&amp;nbsp;JPA나&amp;nbsp;다른&amp;nbsp;데이터&amp;nbsp;접근&amp;nbsp;기술을&amp;nbsp;사용하면,&amp;nbsp;그에&amp;nbsp;맞는&amp;nbsp;다른&amp;nbsp;예외&amp;nbsp; &lt;br /&gt;로&amp;nbsp;변경해야&amp;nbsp;하고,&amp;nbsp;결국&amp;nbsp;서비스&amp;nbsp;코드도&amp;nbsp;수정해야&amp;nbsp;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JDBC 반복 문제
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이 코드들은 유사한 코드들의 반복이 너무 많다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 서비스 계층을 순수하게 유지하면서, 지금까지 이야기한 문제들을 해결할 수 있는 다양한 방법과 기술들을 제공한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;해결1 : 트랜잭션 추상화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 나온 문제들을 해결하려면 트랜잭션 기능을 추상화하면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 추상화 인터페이스&lt;/p&gt;
&lt;pre id=&quot;code_1716410124373&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface TxManager { 
	begin();
	commit(); 
	rollback();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TxManager 인터페이스를 기반으로 각각의 기술에 맞는 구현체를 만들면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JdbcTxManager : JDBC 트랜잭션 기능을 제공하는 구현체&lt;/li&gt;
&lt;li&gt;JpaTxManager : JPA 트랜잭션 기능을 제공하는 구현체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 추상화와 의존관계&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhS5Q5/btsHx9nTgwv/yQ3vTEg5EOkGoEKhXxhOx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhS5Q5/btsHx9nTgwv/yQ3vTEg5EOkGoEKhXxhOx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhS5Q5/btsHx9nTgwv/yQ3vTEg5EOkGoEKhXxhOx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhS5Q5%2FbtsHx9nTgwv%2FyQ3vTEg5EOkGoEKhXxhOx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;958&quot; height=&quot;454&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스는 특정 트랜잭션 기술에 직접 의존하는 것이 아닌, TxManager&amp;nbsp;라는&amp;nbsp;추상화된&amp;nbsp;인터페이스에&amp;nbsp;의존한다.&amp;nbsp; &lt;br /&gt;이제&amp;nbsp;원하는&amp;nbsp;구현체를&amp;nbsp;DI를&amp;nbsp;통해서&amp;nbsp;주입하면&amp;nbsp;된다.&amp;nbsp;예를&amp;nbsp;들어서&amp;nbsp;JDBC&amp;nbsp;트랜잭션&amp;nbsp;기능이&amp;nbsp;필요하면&amp;nbsp; &lt;br /&gt;JdbcTxManager&amp;nbsp;를&amp;nbsp;서비스에&amp;nbsp;주입하고,&amp;nbsp;JPA&amp;nbsp;트랜잭션&amp;nbsp;기능으로&amp;nbsp;변경해야&amp;nbsp;하면&amp;nbsp;JpaTxManager&amp;nbsp;를&amp;nbsp;주입하면&amp;nbsp; &lt;br /&gt;된다.&lt;/li&gt;
&lt;li&gt;서비스는&amp;nbsp;인터페이스에&amp;nbsp;의존하고&amp;nbsp;DI를&amp;nbsp;사용한&amp;nbsp;덕분에&amp;nbsp;OCP&amp;nbsp;원칙을&amp;nbsp;지키게&amp;nbsp;되었다.&amp;nbsp;이제&amp;nbsp;트랜잭션&amp;nbsp; &lt;br /&gt;을&amp;nbsp;사용하는&amp;nbsp;서비스&amp;nbsp;코드를&amp;nbsp;전혀&amp;nbsp;변경하지&amp;nbsp;않고,&amp;nbsp;트랜잭션&amp;nbsp;기술을&amp;nbsp;마음껏&amp;nbsp;변경할&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;스프링 트랜잭션 추상화&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1003&quot; data-origin-height=&quot;463&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0F0zt/btsHyod4D91/EPfy27dDjUf5TBKGBOk6dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0F0zt/btsHyod4D91/EPfy27dDjUf5TBKGBOk6dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0F0zt/btsHyod4D91/EPfy27dDjUf5TBKGBOk6dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0F0zt%2FbtsHyod4D91%2FEPfy27dDjUf5TBKGBOk6dk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1003&quot; height=&quot;463&quot; data-origin-width=&quot;1003&quot; data-origin-height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlatformTransactionManager 인터페이스&lt;/p&gt;
&lt;pre id=&quot;code_1716410303425&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.springframework.transaction;

public interface PlatformTransactionManager extends TransactionManager {
	TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
		throws TransactionException;
	void commit(TransactionStatus status) throws TransactionException; 
	void rollback(TransactionStatus status) throws TransactionException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getTransaction() : 트랜잭션을 시작한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이름이&amp;nbsp;getTransaction()&amp;nbsp;인&amp;nbsp;이유는&amp;nbsp;기존에&amp;nbsp;이미&amp;nbsp;진행중인&amp;nbsp;트랜잭션이&amp;nbsp;있는&amp;nbsp;경우&amp;nbsp;해당&amp;nbsp;트랜잭션에&amp;nbsp;참&amp;nbsp; &lt;br /&gt;여할 수 있기 때문이다.&lt;/li&gt;
&lt;li&gt;참고로&amp;nbsp;트랜잭션&amp;nbsp;참여,&amp;nbsp;전파에&amp;nbsp;대한&amp;nbsp;부분은&amp;nbsp;뒤에서&amp;nbsp;설명한다.&amp;nbsp;지금은&amp;nbsp;단순히&amp;nbsp;트랜잭션을&amp;nbsp;시작하는&amp;nbsp;것으로&amp;nbsp;이&amp;nbsp; &lt;br /&gt;해하면&amp;nbsp;된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;commit() : 트랜잭션을 커밋한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;rollback()&amp;nbsp;:&amp;nbsp;트랜잭션을&amp;nbsp;롤백한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;트랜잭션 동기화&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1017&quot; data-origin-height=&quot;946&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVh8cg/btsHywC7xi1/A3zeWSOvjpP6PzOUPK6zR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVh8cg/btsHywC7xi1/A3zeWSOvjpP6PzOUPK6zR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVh8cg/btsHywC7xi1/A3zeWSOvjpP6PzOUPK6zR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVh8cg%2FbtsHywC7xi1%2FA3zeWSOvjpP6PzOUPK6zR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1017&quot; height=&quot;946&quot; data-origin-width=&quot;1017&quot; data-origin-height=&quot;946&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링은&amp;nbsp;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저를&amp;nbsp;제공한다.&amp;nbsp;이것은&amp;nbsp;쓰레드&amp;nbsp;로컬(&amp;nbsp;ThreadLocal&amp;nbsp;)을&amp;nbsp;사용해서&amp;nbsp;커넥션을&amp;nbsp;동기&amp;nbsp; &lt;br /&gt;화해준다. 트랜잭션 매니저는 내부에서 이 트랜잭션 동기화 매니저를 사용한다.&lt;/li&gt;
&lt;li&gt;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저는&amp;nbsp;쓰레드&amp;nbsp;로컬을&amp;nbsp;사용하기&amp;nbsp;때문에&amp;nbsp;멀티쓰레드&amp;nbsp;상황에&amp;nbsp;안전하게&amp;nbsp;커넥션을&amp;nbsp;동기화&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있&amp;nbsp; &lt;br /&gt;다.&amp;nbsp;따라서&amp;nbsp;커넥션이&amp;nbsp;필요하면&amp;nbsp;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저를&amp;nbsp;통해&amp;nbsp;커넥션을&amp;nbsp;획득하면&amp;nbsp;된다.&amp;nbsp;따라서&amp;nbsp;이전처럼&amp;nbsp;파라미&amp;nbsp; &lt;br /&gt;터로&amp;nbsp;커넥션을&amp;nbsp;전달하지&amp;nbsp;않아도&amp;nbsp;된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동작 방식&lt;/b&gt;&lt;br /&gt;1.&amp;nbsp;&amp;nbsp;&amp;nbsp;트랜잭션을&amp;nbsp;시작하려면&amp;nbsp;커넥션이&amp;nbsp;필요하다.&amp;nbsp;트랜잭션&amp;nbsp;매니저는&amp;nbsp;데이터소스를&amp;nbsp;통해&amp;nbsp;커넥션을&amp;nbsp;만들고&amp;nbsp;트랜잭션을&amp;nbsp; &lt;br /&gt;시작한다. &lt;br /&gt;2.&amp;nbsp;&amp;nbsp;&amp;nbsp;트랜잭션&amp;nbsp;매니저는&amp;nbsp;트랜잭션이&amp;nbsp;시작된&amp;nbsp;커넥션을&amp;nbsp;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저에&amp;nbsp;보관한다. &lt;br /&gt;3.&amp;nbsp;&amp;nbsp;&amp;nbsp;리포지토리는&amp;nbsp;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저에&amp;nbsp;보관된&amp;nbsp;커넥션을&amp;nbsp;꺼내서&amp;nbsp;사용한다.&amp;nbsp;따라서&amp;nbsp;파라미터로&amp;nbsp;커넥션을&amp;nbsp;전달하지&amp;nbsp;않아도&amp;nbsp;된다. &lt;br /&gt;4.&amp;nbsp;&amp;nbsp;&amp;nbsp;트랜잭션이&amp;nbsp;종료되면&amp;nbsp;트랜잭션&amp;nbsp;매니저는&amp;nbsp;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저에&amp;nbsp;보관된&amp;nbsp;커넥션을&amp;nbsp;통해&amp;nbsp;트랜잭션을&amp;nbsp;종료하고, &lt;br /&gt;커넥션도&amp;nbsp;닫는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저&lt;/b&gt; &lt;br /&gt;다음&amp;nbsp;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저&amp;nbsp;클래스를&amp;nbsp;열어보면&amp;nbsp;쓰레드&amp;nbsp;로컬을&amp;nbsp;사용하는&amp;nbsp;것을&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있다. &lt;br /&gt;org.springframework.transaction.support.TransactionSynchronizationManager&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXrIry/btsHzrOqt7m/yW3OUJKSr8u0k1r24RsWgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXrIry/btsHzrOqt7m/yW3OUJKSr8u0k1r24RsWgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXrIry/btsHzrOqt7m/yW3OUJKSr8u0k1r24RsWgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXrIry%2FbtsHzrOqt7m%2FyW3OUJKSr8u0k1r24RsWgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;985&quot; height=&quot;622&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의&amp;nbsp;요청으로&amp;nbsp;서비스&amp;nbsp;로직을&amp;nbsp;실행한다. &lt;br /&gt;1.&amp;nbsp;&amp;nbsp;&amp;nbsp;서비스&amp;nbsp;계층에서&amp;nbsp;transactionManager.getTransaction()&amp;nbsp;을&amp;nbsp;호출해서&amp;nbsp;트랜잭션을&amp;nbsp;시작한다. &lt;br /&gt;2.&amp;nbsp;&amp;nbsp;&amp;nbsp;트랜잭션을&amp;nbsp;시작하려면&amp;nbsp;먼저&amp;nbsp;데이터베이스&amp;nbsp;커넥션이&amp;nbsp;필요하다.&amp;nbsp;트랜잭션&amp;nbsp;매니저는&amp;nbsp;내부에서&amp;nbsp;데이터소스를&amp;nbsp;사용해&amp;nbsp; &lt;br /&gt;서&amp;nbsp;커넥션을&amp;nbsp;생성한다. &lt;br /&gt;3.&amp;nbsp;&amp;nbsp;&amp;nbsp;커넥션을&amp;nbsp;수동&amp;nbsp;커밋&amp;nbsp;모드로&amp;nbsp;변경해서&amp;nbsp;실제&amp;nbsp;데이터베이스&amp;nbsp;트랜잭션을&amp;nbsp;시작한다. &lt;br /&gt;4.&amp;nbsp;&amp;nbsp;&amp;nbsp;커넥션을&amp;nbsp;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저에&amp;nbsp;보관한다. &lt;br /&gt;5.&amp;nbsp;&amp;nbsp;&amp;nbsp;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저는&amp;nbsp;쓰레드&amp;nbsp;로컬에&amp;nbsp;커넥션을&amp;nbsp;보관한다.&amp;nbsp;따라서&amp;nbsp;멀티&amp;nbsp;쓰레드&amp;nbsp;환경에&amp;nbsp;안전하게&amp;nbsp;커넥션을&amp;nbsp;보관&amp;nbsp; &lt;br /&gt;할&amp;nbsp;수&amp;nbsp;있다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDTIAN/btsHxIqym9T/130ZYCLTyyjUrGanBFmvp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDTIAN/btsHxIqym9T/130ZYCLTyyjUrGanBFmvp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDTIAN/btsHxIqym9T/130ZYCLTyyjUrGanBFmvp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDTIAN%2FbtsHxIqym9T%2F130ZYCLTyyjUrGanBFmvp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1009&quot; height=&quot;604&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6.&amp;nbsp;&amp;nbsp;&amp;nbsp;서비스는&amp;nbsp;비즈니스&amp;nbsp;로직을&amp;nbsp;실행하면서&amp;nbsp;리포지토리의&amp;nbsp;메서드들을&amp;nbsp;호출한다.&amp;nbsp;이때&amp;nbsp;커넥션을&amp;nbsp;파라미터로&amp;nbsp;전달하지 &lt;br /&gt;않는다. &lt;br /&gt;7.&amp;nbsp;&amp;nbsp;&amp;nbsp;리포지토리&amp;nbsp;메서드들은&amp;nbsp;트랜잭션이&amp;nbsp;시작된&amp;nbsp;커넥션이&amp;nbsp;필요하다.&amp;nbsp;리포지토리는 &lt;br /&gt;DataSourceUtils.getConnection()&amp;nbsp;을&amp;nbsp;사용해서&amp;nbsp;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저에&amp;nbsp;보관된&amp;nbsp;커넥션을&amp;nbsp;꺼내서&amp;nbsp;사&amp;nbsp; &lt;br /&gt;용한다.&amp;nbsp;이&amp;nbsp;과정을&amp;nbsp;통해서&amp;nbsp;자연스럽게&amp;nbsp;같은&amp;nbsp;커넥션을&amp;nbsp;사용하고,&amp;nbsp;트랜잭션도&amp;nbsp;유지된다. &lt;br /&gt;8.&amp;nbsp;&amp;nbsp;&amp;nbsp;획득한&amp;nbsp;커넥션을&amp;nbsp;사용해서&amp;nbsp;SQL을&amp;nbsp;데이터베이스에&amp;nbsp;전달해서&amp;nbsp;실행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b19t7U/btsHymUPvNR/zy7vapwnQNopXG2gvmkxh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b19t7U/btsHymUPvNR/zy7vapwnQNopXG2gvmkxh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b19t7U/btsHymUPvNR/zy7vapwnQNopXG2gvmkxh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb19t7U%2FbtsHymUPvNR%2Fzy7vapwnQNopXG2gvmkxh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;967&quot; height=&quot;619&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;619&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9.&amp;nbsp;&amp;nbsp;&amp;nbsp;비즈니스&amp;nbsp;로직이&amp;nbsp;끝나고&amp;nbsp;트랜잭션을&amp;nbsp;종료한다.&amp;nbsp;트랜잭션은&amp;nbsp;커밋하거나&amp;nbsp;롤백하면&amp;nbsp;종료된다. &lt;br /&gt;10.&amp;nbsp;&amp;nbsp;&amp;nbsp;트랜잭션을&amp;nbsp;종료하려면&amp;nbsp;동기화된&amp;nbsp;커넥션이&amp;nbsp;필요하다.&amp;nbsp;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저를&amp;nbsp;통해&amp;nbsp;동기화된&amp;nbsp;커넥션을&amp;nbsp;획득한&amp;nbsp; &lt;br /&gt;다. &lt;br /&gt;11.&amp;nbsp;&amp;nbsp;&amp;nbsp;획득한&amp;nbsp;커넥션을&amp;nbsp;통해&amp;nbsp;데이터베이스에&amp;nbsp;트랜잭션을&amp;nbsp;커밋하거나&amp;nbsp;롤백한다. &lt;br /&gt;12.&amp;nbsp;&amp;nbsp;&amp;nbsp;전체 리소스를 정리한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저를&amp;nbsp;정리한다.&amp;nbsp;쓰레드&amp;nbsp;로컬은&amp;nbsp;사용후&amp;nbsp;꼭&amp;nbsp;정리해야&amp;nbsp;한다.&amp;nbsp; &lt;br /&gt;con.setAutoCommit(true)&amp;nbsp;로&amp;nbsp;되돌린다.&amp;nbsp;커넥션&amp;nbsp;풀을&amp;nbsp;고려해야&amp;nbsp;한다. &lt;br /&gt;con.close()&amp;nbsp;를&amp;nbsp;호출해셔&amp;nbsp;커넥션을&amp;nbsp;종료한다.&amp;nbsp;커넥션&amp;nbsp;풀을&amp;nbsp;사용하는&amp;nbsp;경우&amp;nbsp;con.close()&amp;nbsp;를&amp;nbsp;호출하면&amp;nbsp; &lt;br /&gt;커넥션&amp;nbsp;풀에&amp;nbsp;반환된다&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 추상화 덕분에 서비스 코드는 이제 JDBC 기술에 의존하지 않는다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이후 JDBC에서 JPA로 변경해도 서비스 코드를 그대로 유지할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;기술&amp;nbsp;변경시&amp;nbsp;의존관계&amp;nbsp;주입만&amp;nbsp;DataSourceTransactionManager&amp;nbsp;에서&amp;nbsp; &lt;br /&gt;JpaTransactionManager 로 변경해주면 된다.&lt;/li&gt;
&lt;li&gt;java.sql.SQLException 이 아직 남아있지만 이 부분은 뒤에 예외 문제에서 해결하자.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;트랜잭션&amp;nbsp;동기화&amp;nbsp;매니저&amp;nbsp;덕분에&amp;nbsp;커넥션을&amp;nbsp;파라미터로&amp;nbsp;넘기지&amp;nbsp;않아도&amp;nbsp;된다&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;여기서는 DataSourceTransactionManager 의 동작 방식을 위주로 설명했다. 다른 트랜잭션 매니저는 해당 기술에 맞도록 변형되어서 동작한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 문제 해결 - 트랜잭션 템플릿&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복되는 패턴을 어떻게 해결할 수 있는지&lt;/p&gt;
&lt;pre id=&quot;code_1716412068429&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new 
DefaultTransactionDefinition());

try {
//비즈니스 로직
	bizLogic(fromId, toId, money); //이 부분들만 달라짐
    transactionManager.commit(status); //성공시 커밋 
} catch (Exception e) {
    transactionManager.rollback(status); //실패시 롤백 
throw new IllegalStateException(e);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션을 시작하고, 비즈니스 로직을 실행하고, 성공하면 커밋하고, 예외가 발생해서 실패하면 롤백한다.&amp;nbsp; &lt;/li&gt;
&lt;li&gt;다른&amp;nbsp;서비스에서&amp;nbsp;트랜잭션을&amp;nbsp;시작하려면&amp;nbsp;try&amp;nbsp;,&amp;nbsp;catch&amp;nbsp;,&amp;nbsp;finally&amp;nbsp;를&amp;nbsp;포함한&amp;nbsp;성공시&amp;nbsp;커밋,&amp;nbsp;실패시&amp;nbsp;롤백&amp;nbsp;코드가&amp;nbsp; &lt;br /&gt;반복될 것이다.&lt;/li&gt;
&lt;li&gt;이런 형태는 각각의 서비스에서 반복된다. 달라지는 부분은 비즈니스 로직 뿐이다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이럴&amp;nbsp;때&amp;nbsp;템플릿&amp;nbsp;콜백&amp;nbsp;패턴을&amp;nbsp;활용하면&amp;nbsp;이런&amp;nbsp;반복&amp;nbsp;문제를&amp;nbsp;깔끔하게&amp;nbsp;해결할&amp;nbsp;수&amp;nbsp;있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TransactionTemplate&amp;nbsp;을&amp;nbsp;사용하려면&amp;nbsp;transactionManager&amp;nbsp;가&amp;nbsp;필요하다.&amp;nbsp;생성자에서&amp;nbsp; &lt;br /&gt;transactionManager&amp;nbsp;를&amp;nbsp;주입&amp;nbsp;받으면서&amp;nbsp;TransactionTemplate&amp;nbsp;을&amp;nbsp;생성했다&lt;/li&gt;
&lt;li&gt;트랜잭션 템플릿 덕분에, 트랜잭션을 사용할 때 반복하는 코드를 제거할 수 있었다.&lt;/li&gt;
&lt;li&gt;하지만&amp;nbsp;이곳은&amp;nbsp;서비스&amp;nbsp;로직인데&amp;nbsp;비즈니스&amp;nbsp;로직&amp;nbsp;뿐만&amp;nbsp;아니라&amp;nbsp;트랜잭션을&amp;nbsp;처리하는&amp;nbsp;기술&amp;nbsp;로직이&amp;nbsp;함께&amp;nbsp;포함되어&amp;nbsp;있&amp;nbsp; &lt;br /&gt;다.&lt;/li&gt;
&lt;li&gt;애플리케이션을&amp;nbsp;구성하는&amp;nbsp;로직을&amp;nbsp;핵심&amp;nbsp;기능과&amp;nbsp;부가&amp;nbsp;기능으로&amp;nbsp;구분하자면&amp;nbsp;서비스&amp;nbsp;입장에서&amp;nbsp;비즈니스&amp;nbsp;로직은&amp;nbsp;핵심&amp;nbsp; &lt;br /&gt;기능이고, 트랜잭션은 부가 기능이다.&lt;/li&gt;
&lt;li&gt;이렇게&amp;nbsp;비즈니스&amp;nbsp;로직과&amp;nbsp;트랜잭션을&amp;nbsp;처리하는&amp;nbsp;기술&amp;nbsp;로직이&amp;nbsp;한&amp;nbsp;곳에&amp;nbsp;있으면&amp;nbsp;두&amp;nbsp;관심사를&amp;nbsp;하나의&amp;nbsp;클래스에서&amp;nbsp;처리하&amp;nbsp; &lt;br /&gt;게 된다. 결과적으로 코드를 유지보수하기 어려워진다.&lt;/li&gt;
&lt;li&gt;서비스&amp;nbsp;로직은&amp;nbsp;가급적&amp;nbsp;핵심&amp;nbsp;비즈니스&amp;nbsp;로직만&amp;nbsp;있어야&amp;nbsp;한다.&amp;nbsp;하지만&amp;nbsp;트랜잭션&amp;nbsp;기술을&amp;nbsp;사용하려면&amp;nbsp;어쩔&amp;nbsp;수&amp;nbsp;없이&amp;nbsp;트랜&amp;nbsp; &lt;br /&gt;잭션&amp;nbsp;코드가&amp;nbsp;나와야&amp;nbsp;한다.&amp;nbsp;어떻게&amp;nbsp;하면&amp;nbsp;이&amp;nbsp;문제를&amp;nbsp;해결할&amp;nbsp;수&amp;nbsp;있을까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 줄긴했지만...근본적인 문제가 해결되지 않았다는 말이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;트랜잭션 문제 해결 - 트랜잭션 AOP 이해&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지금까지&amp;nbsp;트랜잭션을&amp;nbsp;편리하게&amp;nbsp;처리하기&amp;nbsp;위해서&amp;nbsp;트랜잭션&amp;nbsp;추상화도&amp;nbsp;도입하고,&amp;nbsp;추가로&amp;nbsp;반복적인&amp;nbsp;트랜잭션&amp;nbsp;로직을 &lt;br /&gt;트랜잭션 문제 해결 - 트랜잭션 AOP 이해해결하기 위해 트랜잭션 템플릿도 도입했다.&lt;/li&gt;
&lt;li&gt;트랜잭션 템플릿 덕분에 트랜잭션을 처리하는 반복 코드는 해결할 수 있었다. 하지만&lt;b&gt; 서비스 계층에 순수한 비즈&amp;nbsp; &lt;/b&gt;&lt;br /&gt;&lt;b&gt;니스 로직만 남긴다는 목표는 아직 달성하지 못했다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이럴&amp;nbsp;때&amp;nbsp;스프링&amp;nbsp;AOP를&amp;nbsp;통해&amp;nbsp;프록시를&amp;nbsp;도입하면&amp;nbsp;문제를&amp;nbsp;깔끔하게&amp;nbsp;해결할&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m8G1t/btsHxhmCKhF/FlggvBVCoHXPJTJOg2HwSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m8G1t/btsHxhmCKhF/FlggvBVCoHXPJTJOg2HwSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m8G1t/btsHxhmCKhF/FlggvBVCoHXPJTJOg2HwSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm8G1t%2FbtsHxhmCKhF%2FFlggvBVCoHXPJTJOg2HwSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;457&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;457&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1716412920049&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new 
DefaultTransactionDefinition());
try {
//비즈니스 로직
bizLogic(fromId, toId, money);
    transactionManager.commit(status); //성공시 커밋 
} catch (Exception e) {
    transactionManager.rollback(status); //실패시 롤백throw new IllegalStateException(e);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5YXZA/btsHzbrukOy/gS8SB4KNT3s0k9tHdS7lR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5YXZA/btsHzbrukOy/gS8SB4KNT3s0k9tHdS7lR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5YXZA/btsHzbrukOy/gS8SB4KNT3s0k9tHdS7lR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5YXZA%2FbtsHzbrukOy%2FgS8SB4KNT3s0k9tHdS7lR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1018&quot; height=&quot;451&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;451&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 코드 예시&lt;/p&gt;
&lt;pre id=&quot;code_1716412945034&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class TransactionProxy { 
private MemberService target; 
public void logic() {
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(..); 
try {
//실제 대상 호출
          target.logic();
          transactionManager.commit(status); //성공시 커밋 
        } catch (Exception e) {
          transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
        } 
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1716412996165&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Service {

public void logic() {
	//트랜잭션 관련 코드 제거, 순수 비즈니스 로직만 남음 
	bizLogic(fromId, toId, money);
} 
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 도입 전: 서비스에 비즈니스 로직과 트랜잭션 처리 로직이 함께 섞여있다.&lt;/li&gt;
&lt;li&gt;프록시&amp;nbsp;도입&amp;nbsp;후:&amp;nbsp;트랜잭션&amp;nbsp;프록시가&amp;nbsp;트랜잭션&amp;nbsp;처리&amp;nbsp;로직을&amp;nbsp;모두&amp;nbsp;가져간다.&amp;nbsp;그리고&amp;nbsp;트랜잭션을&amp;nbsp;시작한&amp;nbsp;후에&amp;nbsp;실제&amp;nbsp;서&amp;nbsp; &lt;br /&gt;비스를&amp;nbsp;대신&amp;nbsp;호출한다.&amp;nbsp;트랜잭션&amp;nbsp;프록시&amp;nbsp;덕분에&amp;nbsp;서비스&amp;nbsp;계층에는&amp;nbsp;순수한&amp;nbsp;비즈니즈&amp;nbsp;로직만&amp;nbsp;남길&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;스프링이 제공하는 트랜잭션 AOP&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링이&amp;nbsp;제공하는&amp;nbsp;AOP&amp;nbsp;기능을&amp;nbsp;사용하면&amp;nbsp;프록시를&amp;nbsp;매우&amp;nbsp;편리하게&amp;nbsp;적용할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;스프링&amp;nbsp;핵심&amp;nbsp;원리&amp;nbsp;-&amp;nbsp;고급편을&amp;nbsp; &lt;br /&gt;통해&amp;nbsp;AOP를&amp;nbsp;열심히&amp;nbsp;공부하신&amp;nbsp;분이라면&amp;nbsp;아마도&amp;nbsp;@Aspect&amp;nbsp;,&amp;nbsp;@Advice&amp;nbsp;,&amp;nbsp;@Pointcut&amp;nbsp;를&amp;nbsp;사용해서&amp;nbsp;트랜잭션&amp;nbsp;처&amp;nbsp; &lt;br /&gt;리용 AOP를 어떻게 만들지 머리속으로 그림이 그려질 것이다.&lt;/li&gt;
&lt;li&gt;물론&amp;nbsp;스프링&amp;nbsp;AOP를&amp;nbsp;직접&amp;nbsp;사용해서&amp;nbsp;트랜잭션을&amp;nbsp;처리해도&amp;nbsp;되지만,&amp;nbsp;트랜잭션은&amp;nbsp;매우&amp;nbsp;중요한&amp;nbsp;기능이고,&amp;nbsp;전세계&amp;nbsp;누구&amp;nbsp; &lt;br /&gt;나&amp;nbsp;다&amp;nbsp;사용하는&amp;nbsp;기능이다.&amp;nbsp;스프링은&amp;nbsp;트랜잭션&amp;nbsp;AOP를&amp;nbsp;처리하기&amp;nbsp;위한&amp;nbsp;모든&amp;nbsp;기능을&amp;nbsp;제공한다.&amp;nbsp;스프링&amp;nbsp;부트를&amp;nbsp;사용하&amp;nbsp; &lt;br /&gt;면 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해준다.&lt;/li&gt;
&lt;li&gt;개발자는&amp;nbsp;트랜잭션&amp;nbsp;처리가&amp;nbsp;필요한&amp;nbsp;곳에&amp;nbsp;@Transactional&amp;nbsp;애노테이션만&amp;nbsp;붙여주면&amp;nbsp;된다.&amp;nbsp;스프링의&amp;nbsp;트랜잭션&amp;nbsp; &lt;br /&gt;AOP는 이 애노테이션을 인식해서 트랜잭션 프록시를 적용해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;@Transactional &lt;/b&gt;&lt;br /&gt;&lt;b&gt;org.springframework.transaction.annotation.Transactional&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AOP 적용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@SpringBootTest&amp;nbsp;:&amp;nbsp;스프링&amp;nbsp;AOP를&amp;nbsp;적용하려면&amp;nbsp;스프링&amp;nbsp;컨테이너가&amp;nbsp;필요하다.&amp;nbsp;이&amp;nbsp;애노테이션이&amp;nbsp;있으면&amp;nbsp;테스트&amp;nbsp; &lt;br /&gt;시&amp;nbsp;스프링&amp;nbsp;부트를&amp;nbsp;통해&amp;nbsp;스프링&amp;nbsp;컨테이너를&amp;nbsp;생성한다.&amp;nbsp;그리고&amp;nbsp;테스트에서&amp;nbsp;@Autowired&amp;nbsp;등을&amp;nbsp;통해&amp;nbsp;스프링&amp;nbsp;컨테이&amp;nbsp; &lt;br /&gt;너가 관리하는 빈들을 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;@TestConfiguration&amp;nbsp;:&amp;nbsp;테스트&amp;nbsp;안에서&amp;nbsp;내부&amp;nbsp;설정&amp;nbsp;클래스를&amp;nbsp;만들어서&amp;nbsp;사용하면서&amp;nbsp;이&amp;nbsp;에노테이션을&amp;nbsp;붙이면,&amp;nbsp;스&amp;nbsp; &lt;br /&gt;프링 부트가 자동으로 만들어주는 빈들에 추가로 필요한 스프링 빈들을 등록하고 테스트를 수행할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;TestConfig
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DataSource&amp;nbsp;스프링에서&amp;nbsp;기본으로&amp;nbsp;사용할&amp;nbsp;데이터소스를&amp;nbsp;스프링&amp;nbsp;빈으로&amp;nbsp;등록한다.&amp;nbsp;추가로&amp;nbsp;트랜잭션&amp;nbsp;매니&amp;nbsp; &lt;br /&gt;저에서도 사용한다.&lt;/li&gt;
&lt;li&gt;DataSourceTransactionManager&amp;nbsp;트랜잭션&amp;nbsp;매니저를&amp;nbsp;스프링&amp;nbsp;빈으로&amp;nbsp;등록한다. &lt;br /&gt;스프링이&amp;nbsp;제공하는&amp;nbsp;트랜잭션&amp;nbsp;AOP는&amp;nbsp;스프링&amp;nbsp;빈에&amp;nbsp;등록된&amp;nbsp;트랜잭션&amp;nbsp;매니저를&amp;nbsp;찾아서&amp;nbsp;사용하기&amp;nbsp;때문에&amp;nbsp; &lt;br /&gt;트랜잭션&amp;nbsp;매니저를&amp;nbsp;스프링&amp;nbsp;빈으로&amp;nbsp;등록해두어야&amp;nbsp;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;느낀 점 : RepositoryTest할 때 bean직접 등록해줘야지 오류가 안났는데 이 이유를 알게 되었다..&amp;nbsp; Bean을 등록을 안해주면 스프링 컨테이너를 전혀 사용하고 있지 않았기 때문이다! 나는 뭔가 그냥 스프링이 자동으로 알아서 해주는 줄 ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boWjhS/btsHxJprHYu/NqXvjKE9BjNjOoKnOKbBI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boWjhS/btsHxJprHYu/NqXvjKE9BjNjOoKnOKbBI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boWjhS/btsHxJprHYu/NqXvjKE9BjNjOoKnOKbBI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboWjhS%2FbtsHxJprHYu%2FNqXvjKE9BjNjOoKnOKbBI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1276&quot; height=&quot;178&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 뒤에 CGLIB 붙었다는 것은 실제 멤버 서비스가 아닌, 트랜잭션 프록시라는 것이라고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(질문 : 근데 돌려보면 여러 코드가 막 쏟아져 나오는데 이게 왜 뜨는거지?)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;트랜잭션&amp;nbsp;문제&amp;nbsp;해결&amp;nbsp;-&amp;nbsp;트랜잭션&amp;nbsp;AOP&amp;nbsp;정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;589&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o9D9j/btsHxTexbbE/kMdwcnhpAfcctMQQRicrU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o9D9j/btsHxTexbbE/kMdwcnhpAfcctMQQRicrU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o9D9j/btsHxTexbbE/kMdwcnhpAfcctMQQRicrU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo9D9j%2FbtsHxTexbbE%2FkMdwcnhpAfcctMQQRicrU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1008&quot; height=&quot;589&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;589&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선언적&amp;nbsp;트랜잭션&amp;nbsp;관리&amp;nbsp;vs&amp;nbsp;프로그래밍&amp;nbsp;방식&amp;nbsp;트랜잭션&amp;nbsp;관리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선언적 트랜잭션 관리(Declarative Transaction Management)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Transactional&amp;nbsp;애노테이션&amp;nbsp;하나만&amp;nbsp;선언해서&amp;nbsp;매우&amp;nbsp;편리하게&amp;nbsp;트랜잭션을&amp;nbsp;적용하는&amp;nbsp;것을&amp;nbsp;선언적&amp;nbsp;트랜잭&amp;nbsp; &lt;br /&gt;션 관리라 한다.&lt;/li&gt;
&lt;li&gt;선언적&amp;nbsp;트랜잭션&amp;nbsp;관리는&amp;nbsp;과거&amp;nbsp;XML에&amp;nbsp;설정하기도&amp;nbsp;했다.&amp;nbsp;이름&amp;nbsp;그대로&amp;nbsp;해당&amp;nbsp;로직에&amp;nbsp;트랜잭션을&amp;nbsp;적용하겠다&amp;nbsp;라&amp;nbsp; &lt;br /&gt;고 어딘가에 선언하기만 하면 트랜잭션이 적용되는 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;nbsp;프로그래밍 방식의 트랜잭션 관리(programmatic transaction management)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션&amp;nbsp;매니저&amp;nbsp;또는&amp;nbsp;트랜잭션&amp;nbsp;템플릿&amp;nbsp;등을&amp;nbsp;사용해서&amp;nbsp;트랜잭션&amp;nbsp;관련&amp;nbsp;코드를&amp;nbsp;직접&amp;nbsp;작성하는&amp;nbsp;것을&amp;nbsp;프로그래&amp;nbsp; &lt;br /&gt;밍 방식의 트랜잭션 관리라 한다.&lt;/li&gt;
&lt;li&gt;선언적&amp;nbsp;트랜잭션&amp;nbsp;관리가&amp;nbsp;프로그래밍&amp;nbsp;방식에&amp;nbsp;비해서&amp;nbsp;훨씬&amp;nbsp;간편하고&amp;nbsp;실용적이기&amp;nbsp;때문에&amp;nbsp;실무에서는&amp;nbsp;대부분&amp;nbsp;선언적&amp;nbsp; &lt;br /&gt;트랜잭션 관리를 사용한다.&lt;/li&gt;
&lt;li&gt;프로그래밍 방식의 트랜잭션 관리는 스프링 컨테이너나 스프링 AOP 기술 없이 간단히 사용할 수 있지만 실무에서는 대부분 스프링 컨테이너와 스프링 AOP를 사용하기 때문에 거의 사용되지 않는다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;프로그래밍&amp;nbsp;방식&amp;nbsp;트랜잭션&amp;nbsp;관리는&amp;nbsp;테스트&amp;nbsp;시에&amp;nbsp;가끔&amp;nbsp;사용될&amp;nbsp;때는&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정리&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링이&amp;nbsp;제공하는&amp;nbsp;선언적&amp;nbsp;트랜잭션&amp;nbsp;관리&amp;nbsp;덕분에&amp;nbsp;드디어&amp;nbsp;트랜잭션&amp;nbsp;관련&amp;nbsp;코드를&amp;nbsp;순수한&amp;nbsp;비즈니스&amp;nbsp;로직에서&amp;nbsp;제거할&amp;nbsp; &lt;br /&gt;수 있었다.&lt;/li&gt;
&lt;li&gt;개발자는&amp;nbsp;트랜잭션이&amp;nbsp;필요한&amp;nbsp;곳에&amp;nbsp;@Transactional&amp;nbsp;애노테이션&amp;nbsp;하나만&amp;nbsp;추가하면&amp;nbsp;된다.&amp;nbsp;나머지는&amp;nbsp;스프링&amp;nbsp;트랜&amp;nbsp; &lt;br /&gt;잭션 AOP가 자동으로 처리해준다.&lt;/li&gt;
&lt;li&gt;@Transactional&amp;nbsp;애노테이션의&amp;nbsp;자세한&amp;nbsp;사용법은&amp;nbsp;뒤에서&amp;nbsp;설명한다.&amp;nbsp;지금은&amp;nbsp;전체&amp;nbsp;구조를&amp;nbsp;이해하는데&amp;nbsp;초점&amp;nbsp; &lt;br /&gt;을&amp;nbsp;맞추자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;스프링&amp;nbsp;부트의&amp;nbsp;자동&amp;nbsp;리소스&amp;nbsp;등록&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1716414755095&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@TestConfiguration 
static class TestConfig { 
    @Bean
	DataSource dataSource() {
		return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
    } 
    @Bean
	PlatformTransactionManager transactionManager() {
		return new DataSourceTransactionManager(dataSource());
	}
		@Bean
	MemberRepositoryV3 memberRepositoryV3() { 
		return new MemberRepositoryV3(dataSource());
    } 
    @Bean
	MemberServiceV3_3 memberServiceV3_3() {
		return new MemberServiceV3_3(memberRepositoryV3());	
    } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 이전 실습 처럼 데이터소스와 트랜잭션 매니저를 직접 스프링 빈으로 등록해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트가 제공하는 자동 등록을 이용해 데이터소스와 트랜잭션 매니저를 편리하게 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.properties&lt;/p&gt;
&lt;pre id=&quot;code_1716414844756&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링&amp;nbsp;부트가&amp;nbsp;기본으로&amp;nbsp;생성하는&amp;nbsp;데이터소스는&amp;nbsp;커넥션풀을&amp;nbsp;제공하는&amp;nbsp;HikariDataSource&amp;nbsp;이다.&amp;nbsp;커넥션풀과&amp;nbsp; &lt;br /&gt;관련된 설정도 application.properties 를 통해서 지정할 수 있다.&lt;/li&gt;
&lt;li&gt;spring.datasource.url&amp;nbsp;속성이&amp;nbsp;없으면&amp;nbsp;내장&amp;nbsp;데이터베이스(메모리&amp;nbsp;DB)를&amp;nbsp;생성하려고&amp;nbsp;시도한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션 매니저 - 자동 등록&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링&amp;nbsp;부트는&amp;nbsp;적절한&amp;nbsp;트랜잭션&amp;nbsp;매니저(&amp;nbsp;PlatformTransactionManager&amp;nbsp;)를&amp;nbsp;자동으로&amp;nbsp;스프링&amp;nbsp;빈에&amp;nbsp;등록한&amp;nbsp; &lt;br /&gt;다.&lt;/li&gt;
&lt;li&gt;자동으로 등록되는 스프링 빈 이름: transactionManager&lt;/li&gt;
&lt;li&gt;참고로&amp;nbsp;개발자가&amp;nbsp;직접&amp;nbsp;트랜잭션&amp;nbsp;매니저를&amp;nbsp;빈으로&amp;nbsp;등록하면&amp;nbsp;스프링&amp;nbsp;부트는&amp;nbsp;트랜잭션&amp;nbsp;매니저를&amp;nbsp;자동으로&amp;nbsp;등록하지&amp;nbsp; &lt;br /&gt;않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;어떤 트랜잭션 매니저를 선택할지는 현재 등록된 라이브러리를 보고 판단하는데, JDBC를 기술을 사용하면 DataSourceTransactionManager 를 빈으로 등록하고, JPA를 사용하면 JpaTransactionManager 를 빈으 로 등록한다. 둘다 사용하는 경우 JpaTransactionManager 를 등록한다. 참고로 JpaTransactionManager 는 DataSourceTransactionManager 가 제공하는 기능도 대부분 지원한다&lt;/blockquote&gt;
&lt;pre id=&quot;code_1716415182537&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package hello.jdbc.service;

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.sql.SQLException;

import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
 * 트랜잭션 - Datasource, transactionManager 자동 등록
 */
@Slf4j
@SpringBootTest
class MemberServiceV3_4Test {
    public static final String MEMBER_A = &quot;memberA&quot;;
    public static final String MEMBER_B = &quot;memberB&quot;;
    public static final String MEMBER_EX = &quot;ex&quot;;
    @Autowired
    MemberRepositoryV3 memberRepository;
    @Autowired
    MemberServiceV3_3 memberService;
    @AfterEach
    void after() throws SQLException {
        memberRepository.delete(MEMBER_A);
        memberRepository.delete(MEMBER_B);
        memberRepository.delete(MEMBER_EX);
    }
    @TestConfiguration
    static class TestConfig {
        private final DataSource dataSource;
        public TestConfig(DataSource dataSource) {
            this.dataSource = dataSource;
        }
        @Bean
        MemberRepositoryV3 memberRepositoryV3() {
            return new MemberRepositoryV3(dataSource);
        }
        @Bean
        MemberServiceV3_3 memberServiceV3_3() {
            return new MemberServiceV3_3(memberRepositoryV3());
        }
    }
    @Test
    void AopCheck() {
        log.info(&quot;memberService class={}&quot;, memberService.getClass());
        log.info(&quot;memberRepository class={}&quot;, memberRepository.getClass());
        Assertions.assertThat(AopUtils.isAopProxy(memberService)).isTrue();Assertions.assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();
    }
    @Test
    @DisplayName(&quot;정상 이체&quot;)
    void accountTransfer() throws SQLException {
//given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberB = new Member(MEMBER_B, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberB);
//when
        memberService.accountTransfer(memberA.getMemberId(),
                memberB.getMemberId(), 2000);
//then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberB.getMemberId());
        assertThat(findMemberA.getMoney()).isEqualTo(8000);
        assertThat(findMemberB.getMoney()).isEqualTo(12000);
    }
    @Test
    @DisplayName(&quot;이체중 예외 발생&quot;)
    void accountTransferEx() throws SQLException {
        //given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberEx = new Member(MEMBER_EX, 10000);
        memberRepository.save(memberA);
        memberRepository.save(memberEx);
        //when
        assertThatThrownBy(() -&amp;gt;
                memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(),
                        2000))
                .isInstanceOf(IllegalStateException.class);
        //then
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberEx = memberRepository.findById(memberEx.getMemberId());
            //memberA의 돈이 롤백 되어야함
        assertThat(findMemberA.getMoney()).isEqualTo(10000);
        assertThat(findMemberEx.getMoney()).isEqualTo(10000);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터소스와&amp;nbsp;트랜잭션&amp;nbsp;매니저를&amp;nbsp;스프링&amp;nbsp;빈으로&amp;nbsp;등록하는&amp;nbsp;코드가&amp;nbsp;생략되었다.&amp;nbsp;따라서&amp;nbsp;스프링&amp;nbsp;부트가&amp;nbsp; &lt;br /&gt;application.properties&amp;nbsp;에&amp;nbsp;지정된&amp;nbsp;속성을&amp;nbsp;참고해서&amp;nbsp;데이터소스와&amp;nbsp;트랜잭션&amp;nbsp;매니저를&amp;nbsp;자동으로&amp;nbsp;생성해준&amp;nbsp; &lt;br /&gt;다.&lt;/li&gt;
&lt;li&gt;코드에서 보는 것 처럼 생성자를 통해서 스프링 부트가 만들어준 데이터소스 빈을 주입 받을 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터소스와 트랜잭션 매니저는 스프링 부트가 제공하는 자동 빈 등록 기능을 사용하는 것이 편리하다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;추가로&amp;nbsp;application.properties&amp;nbsp;를&amp;nbsp;통해&amp;nbsp;설정도&amp;nbsp;편리하게&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 개발할 때 내부 메커니즘이 어떻게 작동하는 건지 제대로 몰랐는데 이번 강의를 듣고 내부 메커니즘을 이해할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>_hanbxx_</author>
      <guid isPermaLink="true">https://hanbxx.tistory.com/74</guid>
      <comments>https://hanbxx.tistory.com/74#entry74comment</comments>
      <pubDate>Thu, 23 May 2024 07:05:11 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 1992번 JAVA 쿼드트리 실버 1</title>
      <link>https://hanbxx.tistory.com/72</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1992&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1992&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1716186830671&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;
import java.lang.*;
import java.io.*;

// The main method must be in a class named &quot;Main&quot;.
class Main {
    public static int N;
    public static int[][] arr;
   	public static StringBuilder sb = new StringBuilder();
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        N = Integer.parseInt(br.readLine());
        arr = new int[N][N];

        for (int i = 0; i &amp;lt;N;i++) {
            String[] str = br.readLine().split(&quot;&quot;);
            for (int j =0;j &amp;lt;N;j++) {
                arr[i][j] = Integer.parseInt(str[j]);
            }
        }


        recur(0,0,N);
        
        
        
        System.out.println(sb);
    }
    public static void recur(int x, int y, int size) {
        //압축이 가능한지 - 가능하다면 출력값에 추가
        if (isPossible(x,y,size)) {
            sb.append(arr[x][y]);
            return;
        }

        //압축이 가능하지 않으면 기존 size의 절반에서 시작해서 재귀를 돌아야 한다
        int newSize = size / 2;
        sb.append(&quot;(&quot;);
        recur(x,y,newSize); // 왼쪽 위ㅇ
        recur(x,y+newSize,newSize); //오른쪽 위ㅇ
        recur(x+newSize,y,newSize); //왼쪽 아래ㅇ
        recur(x+newSize,y+newSize,newSize); //오른쪽 아래ㅇ

        sb.append(&quot;)&quot;); 
    }
    public static boolean isPossible(int x, int y, int size) {
        int val = arr[x][y];

        for (int i = x; i &amp;lt; x +size;i++) {
            for (int j = y ; j &amp;lt; y + size; j++) {
                if (val != arr[i][j]) {
                    return false;
                }
            }
        }
        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분할 정복&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 작게 분할한 후 각각을 정복하는 알고리즘.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분할정복 알고리즘은 문제를 작게 나누어 해결하고 이 해결을 결합하여 원래의 문제를 해결한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>_hanbxx_</author>
      <guid isPermaLink="true">https://hanbxx.tistory.com/72</guid>
      <comments>https://hanbxx.tistory.com/72#entry72comment</comments>
      <pubDate>Mon, 20 May 2024 15:37:50 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Reflection 사용하여 중복 코드 줄여보기</title>
      <link>https://hanbxx.tistory.com/71</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;에휴..보이시나요 이 중복되고 드러운 코드 구조를?&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1715756293349&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public void incrementPositiveCineMaster(Popcorn popcorn) {
        popcornQueryRepository.incrementPositiveCineMaster(popcorn);
    }

    public void incrementPositiveGreatFilming(Popcorn popcorn) {
        popcornQueryRepository.incrementPositiveGreatFilming(popcorn);
    }

    public void incrementPositivePom(Popcorn popcorn) {
        popcornQueryRepository.incrementPositivePom(popcorn);
    }

    public void incrementPositiveAnimationIsGood(Popcorn popcorn) {
        popcornQueryRepository.incrementPositiveAnimationIsGood(popcorn);
    }

    public void incrementPositiveArtIsGood(Popcorn popcorn) {
        popcornQueryRepository.incrementPositiveArtIsGood(popcorn);
    }

    public void incrementPositiveCustom(Popcorn popcorn) {
        popcornQueryRepository.incrementPositiveCustom(popcorn);
    }

    public void incrementPositiveMusic(Popcorn popcorn) {
        popcornQueryRepository.incrementPositiveMusic(popcorn);
    }

    public void incrementPositiveTopicIsGood(Popcorn popcorn) {
        popcornQueryRepository.incrementPositiveTopicIsGood(popcorn);
    }

    public void incrementPositiveLinesAreGood(Popcorn popcorn) {
        popcornQueryRepository.incrementPositiveLinesAreGood(popcorn);
    }

    public void incrementPositiveEndingIsGood(Popcorn popcorn) {
        popcornQueryRepository.incrementPositiveEndingIsGood(popcorn);
    }

    public void incrementPositiveCastingIsGood(Popcorn popcorn) {
        popcornQueryRepository.incrementPositiveCastingIsGood(popcorn);
    }

    public void incrementPositiveActingIsGood(Popcorn popcorn) {
        popcornQueryRepository.incrementPositiveActingIsGood(popcorn);
    }

    public void incrementPositiveChemistryIsGood(Popcorn popcorn) {
        popcornQueryRepository.incrementPositiveChemistryIsGood(popcorn);
    }

    public void incrementNegativeIffy(Popcorn popcorn) {
        popcornQueryRepository.incrementNegativeIffy(popcorn);
    }

    public void incrementNegativeBadEditing(Popcorn popcorn) {
        popcornQueryRepository.incrementNegativeBadEditing(popcorn);
    }

    @Transactional
    public void incrementNegativeBadAngle(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornNegativeCount.badAngle,
                        qpopcorn.popcornNegativeCount.badAngle.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementNegativeBadDetail(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornNegativeCount.badDetail,
                        qpopcorn.popcornNegativeCount.badDetail.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementNegativeBadColor(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornNegativeCount.badColor,
                        qpopcorn.popcornNegativeCount.badColor.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementNegativeBadCustom(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornNegativeCount.badCustom,
                        qpopcorn.popcornNegativeCount.badCustom.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementNegativeBadMusic(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornNegativeCount.badMusic,
                        qpopcorn.popcornNegativeCount.badMusic.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementNegativeBadSound(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornNegativeCount.badSound,
                        qpopcorn.popcornNegativeCount.badSound.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementNegativeBadEnding(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornNegativeCount.badEnding,
                        qpopcorn.popcornNegativeCount.badEnding.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementNegativeEndingLoose(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornNegativeCount.endingLoose,
                        qpopcorn.popcornNegativeCount.endingLoose.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementNegativeNoDetail(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornNegativeCount.noDetail,
                        qpopcorn.popcornNegativeCount.noDetail.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementNegativeBadTopic(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornNegativeCount.badTopic,
                        qpopcorn.popcornNegativeCount.badTopic.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementNegativeBadActing(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornNegativeCount.badActing,
                        qpopcorn.popcornNegativeCount.badActing.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementNegativeBadCasting(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornNegativeCount.badCasting,
                        qpopcorn.popcornNegativeCount.badCasting.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementPositiveSetIsArt(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornPositiveCount.setIsArt,
                        qpopcorn.popcornPositiveCount.setIsArt.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementPositiveOst(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(qpopcorn.popcornPositiveCount.ost, qpopcorn.popcornPositiveCount.ost.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }

    @Transactional
    public void incrementPositiveWrittenByGod(Popcorn popcorn) {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        JPAUpdateClause updateClause = jpaQueryFactory.update(qpopcorn);

        updateClause
                .set(
                        qpopcorn.popcornPositiveCount.writtenByGod,
                        qpopcorn.popcornPositiveCount.writtenByGod.add(1))
                .where(qpopcorn.id.eq(popcorn.getId()))
                .execute();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팝콘메이트에서 영화의 &quot;상영지수&quot;라는 평가 지표를 계산해야 할 때, 사용자가 영화에 대한 리뷰를 작성할 때 &quot;키워드 리뷰&quot;를 함께 제공하며 이를 토대로 상영지수를 업데이트해야 하는 복잡한 로직이 필요하다.&lt;br /&gt;&lt;br /&gt;사용자와 영화 간의 중간 엔티티에는 사용자의 키워드 리뷰 선택 여부를 나타내는 키워드 필드가 있으며, 특정 영화 엔티티에는 키워드 개수를 나타내는 필드가 별도로 존재한다.&lt;br /&gt;&lt;br /&gt;이런 구조 때문에 사용자의 키워드 리뷰가 도착할 때, 모든 필드의 선택 여부를 if문으로 판단하고 true인 필드와 이름이 같은 키워드 개수 필드를 업데이트하는 로직이 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 중복 로직을 없애기 위해 자바의 Reflection을 사용해보았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reflection을 사용하면, 저 위에 코드가 단 두가지 메소드로도 구현이 가능해 진다.&lt;/p&gt;
&lt;pre id=&quot;code_1715757199018&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  public void incrementNegative(PopcornNegative negative, Popcorn popcorn) throws NoSuchFieldException, IllegalAccessException {
        popcornQueryRepository.incrementNegative(negative, popcorn);
    }

    public void incrementPositive(PopcornPositive positive, Popcorn popcorn) throws NoSuchFieldException, IllegalAccessException {
        popcornQueryRepository.incrementPositive(positive,popcorn);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Repository의 사용된 increment함수를 더 살펴 보면&lt;/p&gt;
&lt;pre id=&quot;code_1715757249224&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public void incrementPositive(PopcornPositive positive, Popcorn popcorn) throws NoSuchFieldException, IllegalAccessException {
        QPopcorn qpopcorn = QPopcorn.popcorn;
        List&amp;lt;String&amp;gt; chosen = getTrueFields(positive);

        for (String fieldName : chosen) {
            try {
                // fieldName에 해당하는 필드를 가져옵니다.
                Field popcornField = qpopcorn.popcornPositiveCount.getClass().getDeclaredField(fieldName);
                popcornField.setAccessible(true); // 필드에 대한 접근을 허용합니다.

                // 현재 필드의 값을 가져옵니다.
                NumberPath&amp;lt;Integer&amp;gt; fieldValue = (NumberPath&amp;lt;Integer&amp;gt;) popcornField.get(qpopcorn.popcornPositiveCount);

                // 값을 1 증가시킵니다.
                jpaQueryFactory.update(qpopcorn)
                        .set(fieldValue, fieldValue.add(1))
                        .where(qpopcorn.id.eq(popcorn.getId()))
                        .execute();
            } catch (NoSuchFieldException | IllegalAccessException e) {
                // 필드를 찾을 수 없거나 접근할 수 없는 경우 예외 처리
                e.printStackTrace();
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리플렉션은 클래스 메서드 필드 등의 정보를 동적으로 가져오고 조작할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;getDeclaredField(fieldName)&lt;/b&gt; 메서드 : 이를 사용하여 클래스의 필드를 가져온다. 이 메서드는 클래스의 모든 필드를 고려한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;setAccessible(true)&lt;/b&gt; 메서드 : 호출하여 필드에 대한 접근을 허용한다. public말고 protected나 private필드를 사용했을 때 꼭 필요한 메서드이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&amp;nbsp;NumberPath&amp;lt;Integer&amp;gt;&lt;/b&gt;: 이 부분은 가져온 필드의 값을 캐스팅하는 것이다. `popcornField`가 실제로 정수 값을 가지므로 이를 적절한 타입으로 변환하여 사용한다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;b&gt;fieldValue&lt;/b&gt;: 가져온 필드의 값을 저장하는 변수이다. 이 변수는 `NumberPath&amp;lt;Integer&amp;gt;` 타입으로 선언되었으며, 필드의 값이 정수형이라고 가정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1715757361566&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public List&amp;lt;String&amp;gt; getTrueFields(Object keywords) {
        List&amp;lt;String&amp;gt; trueFields = new ArrayList&amp;lt;&amp;gt;();

        // PopcornNegative/Positive 클래스의 모든 필드를 가져와서 순회합니다.
        for (java.lang.reflect.Field field : keywords.getClass().getDeclaredFields()) {
            // 필드의 이름을 가져옵니다.
            String fieldName = field.getName();
            // 필드의 값을 가져옵니다.
            boolean fieldValue = false;
            try {
                field.setAccessible(true);
                fieldValue = field.getBoolean(keywords);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            // 필드의 값이 true이면 리스트에 추가합니다.
            if (fieldValue) {
                trueFields.add(fieldName);
            }
        }

        return trueFields;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 선택한 키워드 필드이름을 &quot;getTrueFields&quot;라는 메소드를 통해 String List에 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리플렉션을 남발해서 쓰는 것이 바람직하지는 않지만 이런식으로 중복된 코드가 남발할 때는 적절하게 사용하여 코드의 중복성을 줄이는 데에 리플렉션이 유용한것 같다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 해야할 점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예외 처리를 단순히 간접적으로 사용하는 메소드에서는 add Exception하지 않는 방법이 존재하는지에 대해 생각해보기&lt;/li&gt;
&lt;li&gt;incrementPositiveCount, incrementNegativeCount의 중복도 줄일 수 있는 방법이 있는지에 대한 방안을 더 생각해보기&lt;/li&gt;
&lt;li&gt;예전의 중복된 코드의 쿼리 성능과 현재 바뀐 쿼리 코드의 성능 차이가 얼마나 있는지에 대한 테스트해보기&lt;/li&gt;
&lt;li&gt;리플렉션의 단점을 고려하여 인터페이스나 더 좋은 방식이 존재하는지 고려해보기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>_hanbxx_</author>
      <guid isPermaLink="true">https://hanbxx.tistory.com/71</guid>
      <comments>https://hanbxx.tistory.com/71#entry71comment</comments>
      <pubDate>Wed, 15 May 2024 16:29:35 +0900</pubDate>
    </item>
    <item>
      <title>VO(Value Object)는 무엇이고 이것을 왜 사용하는 것일까?</title>
      <link>https://hanbxx.tistory.com/70</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 진행 중,&amp;nbsp; 시작일과 종료일을 Embedded 객체로 따로 빼서 VO로 처리하는 코드를 짜보았다. 이 과정에서 VO의 정의 , 사용 이유 그리고 사용법에 대해 배울 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VO의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인(엔티티)에서 한 개 또는 그 이상의 속성들을 묶어 특정 값을 나타내는 객체를 의미한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;VO 사용 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;int, String과 같은 primitive 타입이 도메인 객체를 모델링하기 위해 충분하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 primitive 타입의 기능들을 전부 사용하지 않는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유효성 검사 코드를 객체에 중복되는 부분들이 존재하는 모든 객체에서 검사 코드를 진행해야 할 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;VO 사용 시 제약 조건&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;불변성
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Setter와 가변 로직이 없는 불변 상태여야 한다&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;동등성
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;값이 같다면 동등한 객체로 판단해야 한다&amp;nbsp;&lt;/li&gt;
&lt;li&gt;주소가 달라도 값이 같다면 동등한 객체로 판단해야 한다&lt;/li&gt;
&lt;li&gt;즉, equals &amp;amp; hashCode를 재정의 해야 한다&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;자가 유효성 검사
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;VO를 사용하면 VO 안에서 생성 시에 유효성 검사를 진행한 후에 생성되어야 한다.&lt;/li&gt;
&lt;li&gt;VO 내에 유효성 검사가 보장 되어 있으므로 안전하게 사용할 수 있다&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 적용&lt;/h3&gt;
&lt;pre id=&quot;code_1715682204749&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Period {
    private LocalDateTime startDate;

    private LocalDateTime endDate;

    @Builder(access = AccessLevel.PRIVATE)
    private Period(final LocalDateTime startDate, final LocalDateTime endDate) {
        this.startDate = startDate;
        this.endDate = endDate;
    }

    public static Period createPeriod(LocalDateTime startDate, LocalDateTime endDate) {
        validatePeriod(startDate, endDate);
        return Period.builder().startDate(startDate).endDate(endDate).build();
    }

    private static void validatePeriod(LocalDateTime startDate, LocalDateTime endDate) {
        if (startDate.isAfter(endDate) || startDate.isEqual(endDate)) {
            throw new CustomException(ErrorCode.DATE_PRECEDENCE_INVALID);
        }
    }


	//VO 변경 후 메소드 재정의
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Period that = (Period) o;
        return startDate == that.startDate &amp;amp;&amp;amp; endDate == that.endDate;
    }

    @Override
    public int hashCode() {
        return Objects.hash(startDate, endDate);
    }
}
//VO 적용 전
    public static Recruitment createRecruitment(String name, LocalDateTime startDate, LocalDateTime endDate) {
        validatePeriod(startDate, endDate);
        return Recruitment.builder().name(name).period(period).build();
    }
    
    private static void validatePeriod(LocalDateTime startDate, LocalDateTime endDate) {
        if (startDate.isAfter(endDate) || startDate.isEqual(endDate)) {
            throw new CustomException(ErrorCode.DATE_PRECEDENCE_INVALID);
        }
    }

//VO 적용 후
    public static Recruitment createRecruitment(String name, LocalDateTime startDate, LocalDateTime endDate) {
        Period period = Period.createPeriod(startDate, endDate);
        return Recruitment.builder().name(name).period(period).build();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처 :&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ksh-coding.tistory.com/83&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ksh-coding.tistory.com/83&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1715583927663&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;VO(Value Object)는 무엇일까? 왜 사용할까?&quot; data-og-description=&quot;1. VO(Value Object)란? VO의 의미를 보면 다음과 같다. * VO란 도메인에서 한 개 또는 그 이상의 속성들을 묶어서 특정 값을 나타내는 객체를 의미한다. * 해당 속성들을 primitive 타입이다! (int, boolean, ...&quot; data-og-host=&quot;ksh-coding.tistory.com&quot; data-og-source-url=&quot;https://ksh-coding.tistory.com/83&quot; data-og-url=&quot;https://ksh-coding.tistory.com/83&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/s2gbv/hyV6dAYH2q/iNdSmZxEiUeSCYfRxKue41/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/qO7yn/hyV6dOvEIx/vHFNfqfpLRZrB21lkKRAY0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://ksh-coding.tistory.com/83&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ksh-coding.tistory.com/83&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/s2gbv/hyV6dAYH2q/iNdSmZxEiUeSCYfRxKue41/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/qO7yn/hyV6dOvEIx/vHFNfqfpLRZrB21lkKRAY0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;VO(Value Object)는 무엇일까? 왜 사용할까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. VO(Value Object)란? VO의 의미를 보면 다음과 같다. * VO란 도메인에서 한 개 또는 그 이상의 속성들을 묶어서 특정 값을 나타내는 객체를 의미한다. * 해당 속성들을 primitive 타입이다! (int, boolean, ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ksh-coding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>_hanbxx_</author>
      <guid isPermaLink="true">https://hanbxx.tistory.com/70</guid>
      <comments>https://hanbxx.tistory.com/70#entry70comment</comments>
      <pubDate>Tue, 14 May 2024 21:40:23 +0900</pubDate>
    </item>
  </channel>
</rss>