<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>프로도의 개발 블로그</title>
    <link>https://prodo-developer.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 25 May 2026 21:27:40 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>prodo-developer</managingEditor>
    <image>
      <title>프로도의 개발 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/4590175/attach/6a6e28fe854b4f70919338cad8a74af9</url>
      <link>https://prodo-developer.tistory.com</link>
    </image>
    <item>
      <title>카프카, 레빗엠큐, 레디스 큐의 차이점</title>
      <link>https://prodo-developer.tistory.com/186</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;21&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;21,0,0&quot;&gt;Apache Kafka:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;21,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 환경에 최적화되어 고가용성(High Availability)이 뛰어납니다.&lt;/li&gt;
&lt;li&gt;**오프셋(Offset)**을 통해 소비자가 읽은 위치를 스스로 관리하므로, 여러 소비자가 각기 다른 시점의 데이터를 읽을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;21,1,0&quot;&gt;RabbitMQ:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;21,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;21,1,1,0,0&quot;&gt;라우팅 기능&lt;/b&gt;이 매우 강력합니다 (Exchange 개념). 복잡한 규칙에 따라 메시지를 분배해야 할 때 유리합니다.&lt;/li&gt;
&lt;li&gt;메시지 전달 보장(Confirmation) 기능이 상세합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;21,2,0&quot;&gt;Redis Queue (RQ/List):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;21,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 기반이므로 &lt;b data-index-in-node=&quot;10&quot; data-path-to-node=&quot;21,2,1,0,0&quot;&gt;속도가 매우 빠릅니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;영속성(Persistence)보다는 가볍고 빠른 처리가 필요한 백그라운드 작업에 적합합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>기타 TIP</category>
      <author>prodo-developer</author>
      <guid isPermaLink="true">https://prodo-developer.tistory.com/186</guid>
      <comments>https://prodo-developer.tistory.com/186#entry186comment</comments>
      <pubDate>Mon, 16 Oct 2023 13:53:04 +0900</pubDate>
    </item>
    <item>
      <title>[SPRING] 스프링의 트랜잭션 전파 속성(Transaction propagation)</title>
      <link>https://prodo-developer.tistory.com/175</link>
      <description>&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #f15f5f;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #f15f5f;&quot;&gt;1.&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;트랜잭션의 시작과 종료 및 전파 속성(Transaction Propagation)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;&lt;span&gt;[트랜잭션 전파 속성(Transaction Propagation)이란?&lt;/span&gt;&lt;/span&gt;&amp;nbsp;]&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;트랜잭션 전파 속성&quot;을 이해하려면, 우리가 '트랜잭션'이라는 것이 무엇인지 알아야 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭션은 마치 '작업의 단위'와 같습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 당신이 집에서 방 청소를 하는 것을 생각해봅시다. &lt;br /&gt;방 청소는 크게 세 가지 작업으로 나눌 수 있습니다: 1) 먼지 제거, 2) 가구 정리, 그리고 3) 바닥 닦기.&lt;/p&gt;
&lt;p style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이제 여기서 '트랜잭션 전파 속성'은 이런 작업들을 어떻게 조합할 것인가에 대한 규칙입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, &quot;REQUIRED&quot;라는 전파 속성은 이미 진행 중인 청소 작업(예: 먼지 제거)에 다른 작업(예: 가구 정리)을 추가하는 것입니다. 만약 아무도 청소를 하고 있지 않다면, 새로운 청소 작업을 시작합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;반면에 &quot;REQUIRES_NEW&quot;라는 전파 속성은 무조건 새로운 청소 작업을 시작하는 것입니다. 이미 진행 중인 다른 사람의 청소와 상관 없이 자신만의 새로운 청소(예: 바닥 닦기)를 시작합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 트랜잭션 전파 속성은 여러 개의 작업(트랜잭션)들이 어떻게 서로 관계를 맺고 진행될 지 결정하는 규칙입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Spring이 제공하는 선언적 트랜잭션(트랜잭션 어노테이션, @Transactional)의 장점 중 하나는 여러 트랜잭션을 묶어서 커다란 하나의 트랜잭션 경계를 만들 수 있다는 점이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;작업을 하다보면 기존에 트랜잭션이 진행중일 때 추가적인 트랜잭션을 진행해야 하는 경우가 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;이미 트랜잭션이 진행중일 때 추가 트랜잭션 진행을 어떻게 할지 결정하는 것&lt;/span&gt;이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;전파 속성(Propagation)이다.&lt;/span&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; 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;1328&quot; data-origin-height=&quot;465&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv3Zc1/btssVNF3lUf/gSeKrnkU0oq0lmAv3hB6LK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv3Zc1/btssVNF3lUf/gSeKrnkU0oq0lmAv3hB6LK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv3Zc1/btssVNF3lUf/gSeKrnkU0oq0lmAv3hB6LK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv3Zc1%2FbtssVNF3lUf%2FgSeKrnkU0oq0lmAv3hB6LK%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;1328&quot; height=&quot;465&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;465&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;[&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;물리 트랜잭션과 논리 트랜잭션&lt;/span&gt;&lt;/span&gt;&amp;nbsp;]&lt;/b&gt;&lt;/span&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;span style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot;&gt; 물리 트랜잭션과 논리 트랜잭션의 차이는 &quot;실제 일을 하는 사람들&quot;과 &quot;그 일들을 추적하고 관리하는 사람들&quot; 사이의 차이&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭션은 데이터베이스에서 제공하는 기술이므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;커넥션 객체를 통해 처리&lt;/span&gt;한다. 그래서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;1개의 트랜잭션을 사용한다는 것은 하나의 커넥션 객체를 사용한다는 것&lt;/span&gt;이고, 실제 데이터베이스의 트랜잭션을 사용한다는 점에서 물리 트랜잭션이라고도 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 설명하였듯 트랜잭션 전파 속성에 따라서 외부 트랜잭션과 내부 트랜잭션이 동일한 트랜잭션을 사용할 수도 있다. 하지만 스프링의 입장에서는 트랜잭션 매니저를 통해 트랜잭션을 처리하는 곳이 2군데이다. 그래서 실제 데이터베이스 트랜잭션과 스프링이 처리하는 트랜잭션 영역을 구분하기 위해 스프링은 논리 트랜잭션이라는 개념을 추가하였다. 예를 들어 다음의 그림은 외부 트랜잭션과 내부 트랜잭션이 1개의 물리 트랜잭션(커넥션)을 사용하는 경우이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1337&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRt9jX/btssVKJiOlz/CvRC85Vchev0F38kwXc4Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRt9jX/btssVKJiOlz/CvRC85Vchev0F38kwXc4Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRt9jX/btssVKJiOlz/CvRC85Vchev0F38kwXc4Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRt9jX%2FbtssVKJiOlz%2FCvRC85Vchev0F38kwXc4Bk%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;1337&quot; height=&quot;470&quot; data-origin-width=&quot;1337&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우에는 2개의 트랜잭션 범위가 존재하기 때문에 개별 논리 트랜잭션이 존재하지만, 실제로는 1개의 물리 트랜잭션이 사용된다. 만약 트랜잭션 전파 없이 1개의 트랜잭션만 사용되면 물리 트랜잭션만 존재하고, 트랜잭션 전파가 사용될 때 논리 트랜잭션 개념이 사용된다. 이러한 물리 트랜잭션과 논리 트랜잭션을 정리하면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; 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;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존의 트랜잭션이 진행중일 때 또 다른 트랜잭션이 사용되면 복잡한 상황이 발생한다. 스프링은 논리 트랜잭션이라는 개념을 도입함으로써 상황에 대한 설명을 쉽게 만들고, 다음과 같은 단순한 원칙을 세울수 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; 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;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;더 이해하기 쉽게 청소를 한다고 생각했을때로 가정해보자.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 '물리 트랜잭션'과 '논리 트랜잭션'의 개념을 명확히 이해해야 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;물리 트랜잭션은 실제로 데이터베이스에 변화를 주는 작업 단위입니다. 예를 들어, 당신이 방 청소를 한다고 가정하면, 청소가 완전히 끝나고 방이 깨끗해진 상태가 '커밋'되는 것입니다. 만약 청소 도중에 문제가 생겨서 중단한다면, 그 전 상태로 돌아가는 것이 '롤백'입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;논리 트랜잭션은 스프링에서 관리하는 작업 단위입니다. 이것은 실제 데이터베이스에 변화를 주지 않지만, 어떤 작업들을 수행할 것인지 결정합니다. 예를 들어, 방 청소 프로젝트에서 누가 어떤 일을 할지, 어떻게 진행될지 등을 계획하는 것입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 여기서 복잡한 상황이 발생할 수 있습니다. 이미 진행 중인 청소(트랜잭션)에 다른 사람들도 참여하게 되면 어떻게 될까요? 이런 경우 스프링은 각각의 참여자(논리 트랜잭션)를 따로 관리합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 A와 B 두 사람이 함께 방 청소를 하는 경우라고 생각해보세요. A와 B 모두 자신의 업무가 완료되야만 (모든 논리 트랜잭션이 커밋되야만) 전체적으로 방 청소(물리 트랜잭션)가 완료됩니다. 하지만 A나 B 중 한 명이라도 문제가 생겨서 업무를 완료하지 못한다면 (하나의 논리 트랜잭션이 롤백된다면), 전체적으로 방 청소는 실패하고 처음부터 다시 시작해야 합니다 (물리 트랜잭션은 롤백됨).&lt;/p&gt;
&lt;p style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 스프링은 논리 트랜잭션 개념을 도입함으로써 복잡한 상황을 쉽게 관리하고, 어떤 상황에서도 일관된 규칙을 적용할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;논리 트랜잭션을 기반으로 단순한 원칙을 세움으로써 2개 이상의 트랜잭션을 다루는 경우에 대한 이해가 상당히 쉬워진다. 실제로 트랜잭션들이 마주하는 상황에서 어떠한 전파 속성들이 있는지 살펴보도록 하자.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #f15f5f;&quot;&gt;2.&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;다양한 스프링의&amp;nbsp;&lt;/span&gt;트랜잭션 전파 속성&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;[&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;REQUIRED 속성과 REQUIRES_NEW 속성&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;]&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;스프링에는 7가지 전파 속성이 존재하는데, REQUIRED와 REQUIRES_NEW를 바탕으로 어떻게 진행되는지 살펴보도록 하자.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;REQUIRED와 REQUIRES_NEW를 이해하면 나머지는 응용이 가능하므로, 두 케이스만 자세히 살펴보도록 하자.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 가지를 알기전에 우리가 초등학생에게 설명한다면 어떻게 설명할 것인가??&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;chatGpt에게 도움을 얻어 정리하였는데 이해가 쏙쏙 되었다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭션이라는 것을 생각해보면,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리가 보드 게임(부루마불, 모놀로피)을 할 때의 차례(turn)와 비슷하다고 생각하면 됩니다.&lt;br /&gt;&quot;한&amp;nbsp;사람이&amp;nbsp;차례를&amp;nbsp;가질&amp;nbsp;때까지&amp;nbsp;다른&amp;nbsp;사람들은&amp;nbsp;기다려야&amp;nbsp;해요.&quot; &lt;br /&gt;&lt;b&gt;&quot;REQUIRED&quot;&lt;/b&gt;&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;&lt;br /&gt;&lt;b&gt;&quot;REQUIRES_NEW&quot;&lt;/b&gt;는 이렇게 말합니다: 무조건 새로운 트랜잭션을 시작합니다.&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;것과&amp;nbsp;같습니다. &lt;br /&gt;&lt;br /&gt;그래서&amp;nbsp;REQUIRED와&amp;nbsp;REQUIRES_NEW의&amp;nbsp;주된&amp;nbsp;차이점은 &lt;br /&gt;'현재&amp;nbsp;진행&amp;nbsp;중인&amp;nbsp;트랜잭션(차례)에&amp;nbsp;어떻게&amp;nbsp;반응하느냐'입니다. &lt;br /&gt;REQUIRED는&amp;nbsp;현재&amp;nbsp;트랜잭션에&amp;nbsp;합류하거나&amp;nbsp;필요한&amp;nbsp;경우&amp;nbsp;새로운&amp;nbsp;트랜잭션을&amp;nbsp;생성합니다.&amp;nbsp;반대로&amp;nbsp;REQUIRES_NEW는&amp;nbsp;항상&amp;nbsp;새로운&amp;nbsp;트랜잭션을&amp;nbsp;생성합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 전문가 적으로 분석해 보겠습니다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #111111; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;REQUIRED&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;REQUIRED는 스프링이 제공하는 기본적인(DEFAULT) 전파 속성으로, 기본적으로 2개의 논리 트랜잭션을 묶어 1개의 물리 트랜잭션을 사용하는 것이다. 앞선 예시로 살펴본 경우가 REQUIRED에 해당하며, 내부 트랜잭션은 기존에 존재하는 외부 트랜잭션에 참여하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1337&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2VIhc/btss3JXkNfz/FuFW1BW7LFIoEiwhbz8Nr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2VIhc/btss3JXkNfz/FuFW1BW7LFIoEiwhbz8Nr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2VIhc/btss3JXkNfz/FuFW1BW7LFIoEiwhbz8Nr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2VIhc%2Fbtss3JXkNfz%2FFuFW1BW7LFIoEiwhbz8Nr1%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;1337&quot; height=&quot;470&quot; data-origin-width=&quot;1337&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 참여한다는 것은 외부 트랜잭션을 그대로 이어간다는 뜻이며, 외부 트랜잭션의 범위가 내부까자 확장되는 것이다. 그러므로 내부 트랜잭션은 새로운 물리 트랜잭션을 사용하지 않는다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 트랜잭션 매니저에 의해 관리되는 논리 트랜잭션이 존재하므로 커밋은 내부 1회, 외부 1회해서 총 2회 실행된다. 물론 내부 트랜잭션은 논리 트랜잭션이기 때문에 커밋을 호출해도 즉시 커밋되지는 않고, 외부 트랜잭션이 최종적으로 커밋될 때 실제로 커밋이 된다. 롤백 역시 비슷한데, 내부 트랜잭션에서 롤백을 하여도 즉시 롤백되지 않는다. 물리 트랜잭션이 롤백될 때 실제 롤백이 처리되는데, 논리 트랜잭션들 중에서 1개라도 롤백되었다면 롤백된다. 물리 트랜잭션은 실제 커넥션에 롤백/커밋을 호출하는 것이므로 해당 트랜잭션이 끝나는 것이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 REQUIRED를 정리하면 다음과 같다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #111111; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;REQUIRES_NEW&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;REQUIRES_NEW는 외부 트랜잭션과 내부 트랜잭션을 완전히 분리하는 전파 속성이다. 그래서 2개의 물리 트랜잭션이 사용되며, 각각 트랜잭션 별로 커밋과 롤백이 수행된다. 이를 그림을 표현하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daJk9W/btssULu2c8o/Aafu98FPHhdAAY8Fpd35Ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daJk9W/btssULu2c8o/Aafu98FPHhdAAY8Fpd35Ik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daJk9W/btssULu2c8o/Aafu98FPHhdAAY8Fpd35Ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaJk9W%2FbtssULu2c8o%2FAafu98FPHhdAAY8Fpd35Ik%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;1286&quot; height=&quot;389&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;389&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 개는 서로 다른 물리 트랜잭션이므로, 내부 트랜잭션 롤백이 외부 트랜잭션 롤백에 영향을 주지 않는다. 그러므로 내부 트랜잭션이 롤백 호출은 실제 커넥션에 롤백을 호출하는 것이므로 트랜잭션이 끝나게 된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서로 다른 물리 트랜잭션을 별도로 가진다는 것은 각각의 디비 커넥션이 사용된다는 것이다. 즉, 1개의 HTTP 요청에 대해 2개의 커넥션이 사용되는 것이다. 내부 트랜잭션이 처리 중일때는 꺼내진 외부 트랜잭션이 대기하는데, 이는 데이터베이스 커넥션을 고갈시킬 수 있다. 그러므로 조심해서 사용해야 하며, 만약 REQURES_NEW 없이 해결 가능하다면 대안책(별도의 클래스를 두기 등)을 사용하는 것이 좋다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;REQUIRED와 REQUIRES_NEW를 이해했다면 나머지는 응용이므로, 간단히 어떻게 동작하는지 살펴보도록 하자.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;[&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;다양한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;트랜잭션 전파 속성&lt;/span&gt;&lt;/span&gt;&amp;nbsp;]&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 설명하였듯 스프링은 총 7가지 전파 속성을 제공한다. 각각에 대해 요약해서 정리하면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;REQUIRED&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;SUPPORTS&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;MANDATORY&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;REQUIRES_NEW&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;NOT_SUPPORTED&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;NEVER&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;NESTED&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이것 또한 바로 이해하기 어려울 수 있어 초등학생에게 설명하는것으로 정리하고 넘어가자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;REQUIRED (필요한 경우): 친구가 과자를 먹고 있는데, 당신도 그 친구와 함께 과자를 먹을 수 있습니다. 만약 아무도 과자를 먹고 있지 않다면, 당신은 새로운 팩의 과자를 열어서 먹을 수 있습니다.&lt;/li&gt;
&lt;li&gt;SUPPORTS (지원하는 경우): 당신은 친구가 과자를 먹고 있다면 같이 먹을 수 있지만, 아무도 안 먹는다 해도 상관없습니다. 그냥 현재 상황에 맞춰서 행동합니다.&lt;/li&gt;
&lt;li&gt;MANDATORY (필수적인 경우): 반드시 이미 누군가가 과자를 열어서 먹고 있어야 합니다. 아무도 안 먹는다면, 당신은 문제에 직면하게 됩니다(오류 발생).&lt;/li&gt;
&lt;li&gt;REQUIRES_NEW (새로 필요한 경우): 답사는 다른 사람이 어떤 종류의 과자를 열어서 나눠주더라도, 반드시 자기만의 새로운 팩의 과자를 열어서 자기 혼자만의 시간을 가집니다.&lt;/li&gt;
&lt;li&gt;NOT_SUPPORTED (지원하지 않음): 여러분은 절대로 다른 사람들과 함께 나눠진 과자를 함께 나눠갖거나 새롭게 팩을 꺼내어 서로 나눠갖지 않아야 합니다.&lt;/li&gt;
&lt;li&gt;NEVER (절대 없음): 여러분은 점심시간에만 간식을 준비할 수 있는 규칙이 있는 학교에서 점심시간 외에 간식 시간을 가집니다(오류 발생).&lt;/li&gt;
&lt;li&gt;NESTED (중첩된 경우): 답사는 이미 시작된 간식 시간에 참여하지만, 자신만의 작은 &quot;부&quot; 간식 시간(예: 트렌치 코트 안에서 스틱 과자를 먹음)을 가집니다. 만약 부 간식 시간이 문제가 생긴다면(예: 과자가 다 떨어짐), 그것은 자신의 작은 간식 시간에만 영향을 주고, 전체 간식 시간에는 영향을 주지 않습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #111111; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;REQUIRED&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; 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 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;REQUIRED는 디폴트 속성으로써 모든 트랜잭션 매니저가 지원하는 속성이다. 별도의 설정이 없다면 REQUIRED로 트랜잭션이 진행된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #111111; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SUPPORTS&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; 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 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #111111; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;MANDATORY&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; 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;기존 트랜잭션 없음: IllegalTransactionStateException 예외 발생&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;기존 트랜잭션이 있음: 기존 트랜잭션에 참여함&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #111111; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;REQUIRES_NEW&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; 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 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #111111; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;NOT_SUPPORTED&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; 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 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #111111; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;NEVER&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; 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;기존 트랜잭션이 있음: IllegalTransactionStateException 예외 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #111111; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;NESTED&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; 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 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;NESTED는 이미 진행중인 트랜잭션에 중첩(자식) 트랜잭션을 만드는 것으로, 독립적인 트랜잭션을 만드는 REQUIRES_NEW와 다르다. NESTED에 의한 중첩 트랜잭션은 부모 트랜잭션의 영향(커밋과 롤백)을 받지만, 중첩 트랜잭션이 외부에 영향을 주지는 않는다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&amp;nbsp;중첩 트랜잭션이 롤백 되어도 외부 트랜잭션은 커밋이 가능하지만&amp;nbsp;외부 트랜잭션이 롤백되면 중첩 트랜잭션은 함께 롤백되는 것이다. NESTED는&amp;nbsp;JDBC의 savepoint 기능을 사용하는데, DB 드라이버가 이를 지원하는지 확인이 필요하며 JPA에서 사용이 불가능하다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #f15f5f;&quot;&gt;3.&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;트랜잭션의 전파 속성 요약&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;[&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;트랜잭션의&amp;nbsp;전파&amp;nbsp;속성&amp;nbsp;요약&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;]&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1499&quot; data-origin-height=&quot;789&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GE93R/btstay8qAeT/z8u78kaEW95XLLLViiqcxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GE93R/btstay8qAeT/z8u78kaEW95XLLLViiqcxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GE93R/btstay8qAeT/z8u78kaEW95XLLLViiqcxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGE93R%2Fbtstay8qAeT%2Fz8u78kaEW95XLLLViiqcxk%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;1499&quot; height=&quot;789&quot; data-origin-width=&quot;1499&quot; data-origin-height=&quot;789&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;참조 링크: &lt;a href=&quot;https://mangkyu.tistory.com/269&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://mangkyu.tistory.com/269&lt;/a&gt;&lt;/p&gt;</description>
      <category>SPRING/기본 상식</category>
      <author>prodo-developer</author>
      <guid isPermaLink="true">https://prodo-developer.tistory.com/175</guid>
      <comments>https://prodo-developer.tistory.com/175#entry175comment</comments>
      <pubDate>Mon, 4 Sep 2023 23:32:36 +0900</pubDate>
    </item>
    <item>
      <title>[SPRING]  생성자 주입을 사용해야 하는 이유, 필드인젝션이 좋지 않은 이유</title>
      <link>https://prodo-developer.tistory.com/152</link>
      <description>&lt;h3 id=&quot;개요&quot; data-ke-size=&quot;size23&quot;&gt;개요&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dependency Injection (의존관계 주입) 이란
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Setter Based Injection (수정자를 통한 주입)&lt;/li&gt;
&lt;li&gt;Constructor based Injection (생성자를 통한 주입)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스프링에서 사용할 수 있는 DI 방법 세가지&lt;/li&gt;
&lt;li&gt;생성자 주입을 이용한 순환참조 방지&lt;/li&gt;
&lt;li&gt;생성자 주입이 테스트 코드 작성하기 좋은 이유&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;서론&quot; data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존관계 주입을 받을때는 아무생각없이 당연하게&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Autowired&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용한 필드주입 방식을 사용해왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 어느날 갑자기(?) 인텔리제이에서 경고메시지를 보여준다는 것을 보게 되었다. 항상 경고는 표시되고 있었겠지만 무시하다가 갑자기 궁금해졌다. 필드인젝션을 사용하고 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Autowired&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 하이라이트 표시가 되면서 나오는 경고메시지는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Field injection is not recommended &amp;hellip;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Always&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;use constructor based dependency injection in your beans&lt;/p&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;Why???? Always???&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 의존관계 주입에 대한 정리한 내용은 아래를 참조하시면 종류별로 확인이 가능합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a href=&quot;https://prodo-developer.tistory.com/120&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;다양한 의존관계 주입 방법&lt;/b&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;h3 id=&quot;dependency-injection-의존관계-주입&quot; data-ke-size=&quot;size23&quot;&gt;Dependency Injection (의존관계 주입)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유를 알기 위해서는 DI 에 대한 이해가 필요하다. DI 는 스프링에서만 사용되는 용어가 아니라 객체지향 프로그래밍에서는 어디에서나 통용되는 개념이다.&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;span&gt;&amp;nbsp;&lt;/span&gt;강한 결합도를 가지는 구조이다. A 클래스 내부에서 B 라는 객체를 직접 생성하고 있다면, B 객체를 C 객체로 바꾸고 싶은 경우에 A 클래스도 수정해야 하는 방식이기 때문에 강한 결합이다.&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;span&gt;&amp;nbsp;&lt;/span&gt;런타임시에 의존관계가 결정되기 때문에 유연한 구조를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOLID 원칙에서 O 에 해당하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Open Closed Principle&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 지키기 위해서 디자인 패턴 중 전략패턴을 사용하게 되는데, 생성자 주입을 사용하게 되면 전략패턴을 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;setter-based-injection-수정자를-통한-주입&quot; data-ke-size=&quot;size23&quot;&gt;Setter Based Injection (수정자를 통한 주입)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존관계 주입에는 크게&lt;span&gt;&amp;nbsp;&lt;/span&gt;생성자 주입, 수정자 주입&lt;span&gt;&amp;nbsp;&lt;/span&gt;두가지 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 한번 보자. 클래스나 인터페이스 이름만 Controller, Service, ServiceImpl 로 지정했지 스프링과는 상관이 없는 순수 자바로만 짜여진 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 수정자를 이용한 의존관계 주입을 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1624326209740&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Controller {
    private Service service;

    public void setService(Service service) {
        this.service = service;
    }

    public void callService() {
    	if(service == null) {
            throw new IllegalStateException(&quot;Service is null. Please set the Service instance using setService() method.&quot;);
        }
    
        service.doSomething();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1624326213388&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Service {
    void doSomething();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1624326226660&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ServiceImpl implements Service {
    @Override
    public void doSomething() {
        System.out.println(&quot;ServiceImpl is doing something&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1624326233690&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Controller controller = new Controller();

        // 어떤 구현체이든, 구현체가 어떤방법으로 구현되든 Service 인터페이스를 구현하기만 하면 된다.
        controller.setService(new ServiceImpl1());
        controller.setService(new ServiceImpl2());

        controller.setService(new Service() {
            @Override
            public void doSomething() {
                System.out.println(&quot;Anonymous class is doing something&quot;);
            }
        });

        controller.setService(
          () -&amp;gt; System.out.println(&quot;Lambda implementation is doing something&quot;)
        );

        // 어떻게든 구현체를 주입하고 호출하면 된다.
        controller.callService();
    }
}&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;(참고) 익명클래스나 람다로 구현할 수 있었던 것은 Service 인터페이스가 함수형 인터페이스이기 때문이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller&lt;span&gt;&amp;nbsp;&lt;/span&gt;클래스의 callService() 메소드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Service 타입의 객체에 의존하고 있다.&lt;/li&gt;
&lt;li&gt;Service 는 인터페이스이고, 인터페이스는 인스턴스화 할 수 없으므로 인터페이스의 구현체가 필요하다.&lt;/li&gt;
&lt;li&gt;Service 인터페이스를 구현하기만 했다면 어떤 타입의 객체라도 Controller 에서 사용할 수 있는데 (다형성) Controller 는 이 구현체의 내부 동작을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;아무 것도 알지 못하고 알 필요도 없다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;main 함수에서 Controller 클래스를 사용하는 것을 보면, 수정자 메소드인 setService() 에 Service 인터페이스의 구현체만 넘겨주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어떤 구현체이든, 구현체가 어떤방법으로 구현되든, Service 인터페이스를 구현하기만 하면 된다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신박하다?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정자 주입으로 의존관계 주입은 런타임시에 할 수 있도록&lt;span&gt;&amp;nbsp;&lt;/span&gt;낮은 결합도를 가지게 구현되었다. 하지만 문제는 수정자를 통해서 Service 의 구현체를 주입해주지 않아도 Controller 객체는 생성가능하다. Controller 객체가 생성가능하다는 것은 내부에 있는 callService() 메소드도 호출 가능하다는 것인데, callService() 메소드는 service.doSomething() 을 호출하고 있으므로&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NullPointerException 이 발생한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주입이 필요한 객체가 주입이 되지 않아도 얼마든지 객체를 생성할 수 있다는 것이 문제다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot;&gt;위 코드에서 &lt;/span&gt;setService()&lt;span style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot;&gt; 메소드는 수정자 주입 방식으로 Service 객체를 주입하는 메소드입니다. &lt;/span&gt;callService()&lt;span style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot;&gt; 메소드에서는 &lt;/span&gt;service&lt;span style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot;&gt; 객체의 null 여부를 검사하여 null이라면 IllegalStateException을 발생시키도록 합니다. 이렇게 예외처리를 함으로써, 런타임시에 NullPointerException이 발생하는 것을 막을 수 있습니다.&lt;/span&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;h3 id=&quot;constructor-based-injection-생성자를-통한-주입&quot; data-ke-size=&quot;size23&quot;&gt;Constructor based Injection (생성자를 통한 주입)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller 에 setter 를 없애고, 생성자를 이용해서 주입한다.&lt;/p&gt;
&lt;pre id=&quot;code_1624326263028&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Controller {
    private Service service;

    public Controller(Service service) {
        this.service = service;
    }

    public void callService() {
        service.doSomething();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 생성자 주입을 해주면 사용하는 쪽은 아래와 같이 바뀐다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1624326275065&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {

        // Controller controller = new Controller(); // 컴파일 에러

        Controller controller1 = new Controller(new ServiceImpl());
        Controller controller2 = new Controller(
            () -&amp;gt; System.out.println(&quot;Lambda implementation is doing something&quot;)
        );
        Controller controller3 = new Controller(new Service() {
            @Override
            public void doSomething() {
                System.out.println(&quot;Anonymous class is doing something&quot;);
            }
        });

        controller1.callService();
        controller2.callService();
        controller3.callService();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 두가지 이득과 한가지 보너스 이득이 생긴다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;null 을 주입하지 않는 한&lt;span&gt;&amp;nbsp;&lt;/span&gt;NullPointerException 은 발생하지 않는다.&lt;/li&gt;
&lt;li&gt;의존관계 주입을 하지 않은 경우에는 Controller&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체를 생성할 수 없다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;즉, 의존관계에 대한 내용을 외부로 노출시킴으로써 컴파일 타임에 오류를 잡아낼 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보너스 이득은 final 을 사용할 수 있다는 것이다. final 로 선언된 레퍼런스타입 변수는 반드시 선언과 함께 초기화가 되어야 하므로 setter 주입시에는 의존관계 주입을 받을 필드에 final 을 선언할 수 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1624326355462&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Controller {
    private final Service service; // final 추가

    public Controller(Service service) {
        this.service = service;
    }

    public void callService() {
        service.doSomething();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;final 의 장점은 누군가가 Controller 내부에서 service 객체를 바꿔치기 할 수 없다는 점이다.&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;h3 id=&quot;스프링에서의-di-방법-세가지&quot; data-ke-size=&quot;size23&quot;&gt;스프링에서의 DI 방법 세가지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서는 수정자 주입, 생성자 주입과 더불어 필드 주입이란걸 할 수 있다. 필드 주입은 수정자를 통한 주입과 유사한 방식으로 이루어지기 때문에,&lt;span&gt;&amp;nbsp;&lt;/span&gt;수정자를 통한 주입의 단점은 Field Injection 을 사용할 때의 단점을 그대로 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더불어, 수정자 주입은 스프링 컨테이너가 아닌 외부에서 수정자를 호출해서 주입할 수 있는 방법이라도 열려있지만, 필드주입은 스프링 컨테이너 말고는 외부에서 주입할 수 있는 방법이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 각 DI 방법에 대한 간단한 예제다. 뒤에서도 쓰기 위해서 예제를 Student, Course 관련된 내용으로 변경했다.&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;Field Injection&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1624326376358&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private CourseService courseService;

    @Override
    public void studentMethod() {
        courseService.courseMethod();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Setter based Injection&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1624326387685&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class StudentServiceImpl implements StudentService {

    private CourseService courseService;

    @Autowired
    public void setCourseService(CourseService courseService) {
        this.courseService = courseService;
    }

    @Override
    public void studentMethod() {
        courseService.courseMethod();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Constructor based Injection&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1624326413409&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class StudentServiceImpl implements StudentService {

    private final CourseService courseService;

    @Autowired
    public StudentServiceImpl(CourseService courseService) {
        this.courseService = courseService;
    }

    @Override
    public void studentMethod() {
        courseService.courseMethod();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인텔리제이에서 보여주는 경고메시지는 위 두 예제 중 아래에 있는 Constructor based Injection 을 사용하라는 것이다.&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;1.NullPointerException&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 방지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot;&gt;객체가 생성될 때 모든 필수 의존성이 주입되므로, NullPointerException과 같은 런타임 예외가 발생할 가능성이 줄어듭니다. 의존성이 null인 상태로 객체가 생성되는 것을 방지할 수 있습니다.&lt;/span&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;2. 주입받을 필드를&lt;span&gt;&amp;nbsp;&lt;/span&gt;final&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 선언 가능하다. &lt;b&gt;불변(Immutable) 객체&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot;&gt;Constructor Injection을 사용하면 주입받을 필드를 &lt;/span&gt;final&lt;span style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot;&gt;로 선언할 수 있습니다. 이렇게 하면 한 번 설정된 값을 변경할 수 없으므로, 객체의 상태를 보호하고 예기치 않은 변경을 방지하는 불변(Immutable) 객체를 만들 수 있습니다.&lt;/span&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;3. &lt;b&gt;의존성 추적 및 테스트 용이성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;-&amp;nbsp;&lt;/b&gt;&lt;span style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot;&gt;Constructor Injection은 클래스의 생성자 시그니처를 통해 어떤 의존성이 필요한지 명확하게 드러나기 때문에 코드를 이해하고 디버깅하기 쉽습니다. 또한, 단위 테스트 작성 시에도 의존성을 쉽게 대체(Mocking 등)할 수 있어 테스트 용이성이 좋아집니다.&lt;/span&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;4.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;의존성 순환 해결&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- &lt;/b&gt;&lt;span style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot;&gt;&amp;nbsp;&lt;span style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot;&gt;생성자에서 모든 의존성을 한 번에 주입하기 때문에, 순환 참조로 인한 초기화 문제를 방지할 수 있습니다.&lt;/span&gt;&lt;/span&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;5.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;강력한 컴파일 타임 검사&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- &lt;span style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot;&gt; Constructor Injection은 컴파일 타임에 많은 종류의 오류를 검출하는 강력한 타입 체크 기능을 제공합니다. 올바른 타입의 인스턴스만 주입될 수 있도록 보장됩니다.&lt;/span&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;생성자-주입을-이용한-순환참조-방지&quot; data-ke-size=&quot;size23&quot;&gt;생성자 주입을 이용한 순환참조 방지&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발하다보면 여러 서비스들 간에 의존관계가 생기게 되는 경우가 있다. 이 예제에서는 CourseService 에서 StudentService 에 의존하고, StudentService 가 CourseService 에 의존하는 경우를 볼 것이다.&lt;/p&gt;
&lt;h3 id=&quot;생성자-주입을-이용한-순환참조-방지&quot; 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;b&gt;Field Injection 의 경우&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1624326469149&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface CourseService {
    void courseMethod();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1624326471989&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class CourseServiceImpl implements CourseService {

    @Autowired
    private StudentService studentService;

    @Override
    public void courseMethod() {
        studentService.studentMethod();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1624326474733&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface StudentService {
    void studentMethod();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1624326477065&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private CourseService courseService;

    @Override
    public void studentMethod() {
        courseService.courseMethod();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 상황은 StudentServiceImple 의 studentMethod() 는 CourseServiceImpl 의 courseMethod() 를 호출하고, CourseServiceImpl 의 courseMethod() 는 StudentServiceImple 의 studentMethod() 를 호출하고 있는 상황이다. 서로서로 주거니 받거니 호출을 반복하면서 끊임없이 호출하다가 결국&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;StackOverflowError&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 발생시키고 죽는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1624326566597&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2019-08-28 00:14:56.042 ERROR 46104 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.StackOverflowError] with root cause


java.lang.StackOverflowError: null
    at com.yaboong.alterbridge.tmp.CourseServiceImpl.courseMethod(CourseServiceImpl.java:26) ~[classes/:na]
    at com.yaboong.alterbridge.tmp.StudentServiceImpl.studentMethod(StudentServiceImpl.java:25) ~[classes/:na]
    at com.yaboong.alterbridge.tmp.CourseServiceImpl.courseMethod(CourseServiceImpl.java:26) ~[classes/:na]
    at com.yaboong.alterbridge.tmp.StudentServiceImpl.studentMethod(StudentServiceImpl.java:25) ~[classes/:na]
    at com.yaboong.alterbridge.tmp.CourseServiceImpl.courseMethod(CourseServiceImpl.java:26) ~[classes/:na]
&amp;hellip;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 순환참조의 문제인데, 실제 코드가 호출이 되기 전까지는 아무것도 알지 못한다. 스프링 애플리케이션 구동도 너무나 잘된다. 여기서 궁금했던게 하나 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 빈 생성이 잘 되는거지&amp;hellip;?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정자 주입이나 필드 주입시에 스프링&lt;span&gt;&amp;nbsp;&lt;/span&gt;ApplicationContext&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 통해서 현재 로딩된 빈 목록을 출력하면 사이클 호출 로직을 가진 두개의 빈이 모두 떠있는 것을 확인할 수 있었다. 아니 사이클 호출을 하고 있는데 빈이 어떻게 생성될 수 있는거지? 생성은 안하고 빈 목록만 가지고 있다가 lazy 로딩하는 방식인건가? 근데 따로 lazy init 옵션을 주지 않으면 lazy 로딩은 적용 되지 않는다던데&amp;hellip;?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체생성시점에서 순환참조가 일어나는 것과&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체생성 후 비즈니스 로직상에서 순환참조가 일어나는 것은 완전히 다른 이야기인데, 하나로 묶어서 생각하고 있었기 때문에 이런 이상한 질문에 빠졌던 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드 주입이나, 수정자 주입은 객체 생성시점에는 순환참조가 일어나는지 아닌지 발견할 수 있는 방법이 없다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Constructor based Injection 의 경우&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1624326591469&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class CourseServiceImpl implements CourseService {

    private final StudentService studentService;

    @Autowired
    public CourseServiceImpl(StudentService studentService) {
        this.studentService = studentService;
    }

    @Override
    public void courseMethod() {
        studentService.studentMethod();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1624326597196&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class StudentServiceImpl implements StudentService {

    private final CourseService courseService;

    @Autowired
    public StudentServiceImpl(CourseService courseService) {
        this.courseService = courseService;
    }

    @Override
    public void studentMethod() {
        courseService.courseMethod();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 경우에도 애플리케이션이 구동이 잘 될까? 실행해보면 아래와 같은 로그가 찍히면서 앱 구동이 실패한다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1624326619165&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  courseServiceImpl defined in file [/Users/yaboong/.../CourseServiceImpl.class]
&amp;uarr;     &amp;darr;
|  studentServiceImpl defined in file [/Users/yaboong/.../StudentServiceImpl.class]
└─────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;빈 생성시 아래와 같은 로직이 수행되면서 어떤 시점에 스프링이 그것을 캐치해서 순환참조라고 알려주는 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1624326664034&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;new CourseServiceImpl(new StudentServiceImpl(new CourseServiceImpl(new ...)))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 생성자 주입을 사용하면 객체 간 순환참조를 하고 있는 경우에 스프링 애플리케이션이 구동되지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너가 빈을 생성하는 시점에서 객체생성에 사이클관계가 생기기 때문이다!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 방안으로는 여러 가지가 있을 수 있는데, 그 중 한 가지 방법은 아래와 같습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;서비스 분리&lt;/b&gt;: 가능하다면 서비스의 책임을 재구성하여 순환 참조를 제거하는 것이 가장 바람직합니다. 현재 CourseService와 StudentService가 서로에게 의존하고 있는데, 이것은 각각의 서비스가 너무 많은 책임을 가지고 있음을 나타낼 수 있습니다. 역할과 책임에 따라 더 세분화된 서비스로 분리하여 순환 참조를 제거할 수도 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #1e1e1e; color: #dcdcdc; text-align: left;&quot;&gt;&lt;code&gt;@Service
public class CourseServiceImpl implements CourseService {

    private final StudentService studentService;

    @Autowired
    public CourseServiceImpl(StudentService studentService) {
        this.studentService = studentService;
    }

    @Override
    public void courseMethod() {
        //student service method call here...
    }
}

@Service
public class StudentServiceImpl implements StudentService {

    private CourseServiceImpl courseServiceImpl;

    @Autowired
    public void setCourseServiceImpl(CourseServiceImpl courseServiceImpl){
        this.courseServiceImpl = courseServiceImpl;
     }

     @Override
     public void studentMethod() {
         //course service method call here...
     }
}&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;위 예제에서 보듯이, 생성자 대신 setter 메소드를 사용하여 한 쪽의 의존성(CourseServiceImpl)을 주입하면 순환 참조 문제를 해결할 수 있습니다. 이 경우 CourseServiceImpl이 먼저 생성되고, 그 다음에 StudentServiceImpl이 생성되며, 마지막으로 StudentServiceImpl에 대한 참조를 가진 CourseServiceImpl의 setter 메소드가 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 두 서비스 사이의 순환 참조 문제를 해결할 수 있지만, 앞서 언급한 바와 같이 일부 단점들도 함께 고려해야 합니다. 특히 Setter Injection을 사용할 때는 객체가 일시적으로 불완전한 상태에 있을 수 있다는 점과 필드에 final 키워드를 사용하여 불변성을 보장할 수 없다는 점 등을 유의해야 합니다.&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;수정자 주입을 사용하면 아주 잘 구동되고 순환참조를 하고 있는 부분에 대한 호출이 이루어질 경우 StackOverflowError 를 뱉기 때문에, 오류를 뱉을 수 밖에 없는 로직을 품고 애플리케이션이 구동되는 것이다.&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;span style=&quot;background-color: #6446ff; color: #ffffff; text-align: left;&quot;&gt;Constructor Injection만으로는 해결할 수 없는 걸까?&lt;/span&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;순환 참조 문제는 생성자 주입(Constructor Injection)만으로는 해결하기 어렵습니다. 생성자 주입은 객체가 생성될 때 모든 의존성을 주입받아야 하기 때문에, 순환 참조가 발생하면 서로가 서로를 기다리게 되어 결국 애플리케이션이 구동되지 않게 됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하는 가장 바람직한 방법은 설계 단계에서 순환 참조를 피하는 것입니다. 이는 객체 지향 설계 원칙 중 하나인 단일 책임 원칙(Single Responsibility Principle)과 관련이 있습니다. 한 클래스 또는 모듈이 너무 많은 책임을 가지고 있다면, 그것들을 분리하여 각각의 클래스 또는 모듈이 자신의 책임만을 가지도록 하는 것이 좋습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #f2f7ff; color: #505567; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그러나 만약 이런 설계 변경이 어렵거나 불가능한 경우에는 필드 주입(Field Injection) 혹은 Setter Injection과 같은 다른 의존성 주입 방식을 사용하여 순환 참조 문제를 해결할 수 있습니다. 이러한 방식들은 순환 참조 문제를 회피할 수 있으나, 앞서 언급한 바와 같이 일부 단점들도 함께 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;테스트-코드-작성하기-좋다&quot; 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;CourserServiceImpl&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 가진 메소드들에 대해서 단위테스트를 수행하고 싶은 경우, field injection 을 사용해서 작성된 클래스라면 단위테스트시 의존관계를 가지는 객체를 생성해서 주입할 수가 없다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;할 수 있는 방법이 없다!&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;스프링의 IoC 컨테이너가 다 생성해서 주입해 주는 방식이고 외부로 노출되어 있는 것이 하나도 없기 때문이다. 그래서 의존관계를 가지고 있는 메소드의 단위테스트를 작성하면 (courseMethod() 같은)&lt;span&gt;&amp;nbsp;&lt;/span&gt;NullPointerException&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, constructor based injection 을 사용해 작성된 클래스라면&lt;span&gt;&amp;nbsp;&lt;/span&gt;CourseServiceImpl&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체를 생성할 때 원하는 구현체를 넘겨주면 되고, 구현체를 넘겨주지 않은 경우에는 객체생성 자체가 불가능하기 때문에 테스트하기도 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;요약&quot; data-ke-size=&quot;size23&quot;&gt;요약&lt;/h3&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;의존관계 설정이 되지 않으면 객체생성 불가 -&amp;gt; 컴파일 타임에 인지 가능, NPE 방지&lt;/li&gt;
&lt;li&gt;의존성 주입이 필요한 필드를 final 로 선언가능 -&amp;gt; Immutable&lt;/li&gt;
&lt;li&gt;(스프링에서) 순환참조 감지가능 -&amp;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;편하다는 것 말고는 없다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;요약&quot; 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조사이트 : &lt;a href=&quot;https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>SPRING/기본 상식</category>
      <author>prodo-developer</author>
      <guid isPermaLink="true">https://prodo-developer.tistory.com/152</guid>
      <comments>https://prodo-developer.tistory.com/152#entry152comment</comments>
      <pubDate>Tue, 29 Aug 2023 00:12:51 +0900</pubDate>
    </item>
    <item>
      <title>[SPRING] 스프링 배치에서 사용되는 Quartz 배치 스케줄링</title>
      <link>https://prodo-developer.tistory.com/174</link>
      <description>&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 &lt;span style=&quot;color: #000000;&quot;&gt;Job &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 중에 오류가 발생할 경우 &lt;/span&gt;예외처리로 FaultTolerant(내결함성)를 통해 pass를 시킬수 있지만&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;1. ItemReader, &lt;span style=&quot;color: #000000;&quot;&gt;ItemProcess, ItemWriter&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에서 설정된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Exception&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 발생했을 경우 (Skip)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. &lt;span style=&quot;color: #000000;&quot;&gt;ItemProcess, ItemWriter&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에서 설정된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Exception&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 발생했을 경우 (Retry)&lt;/span&gt;&lt;/span&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;따라서 job이 실행될 때 여부와 상관없이 가장 먼저 스케줄링을 통해 제어하는 방식이 더 효율적이라 볼 수 있다.&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;h3 data-ke-size=&quot;size23&quot;&gt;쿼츠(Quartz)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;오픈소스 실시간 스케줄링&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버 간 클러스터 기능&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222;&quot;&gt;자바 환경의 규모와 상관없이 사용이 가능하고 Job 실행에 유용한 Spring boot 지원과 같이 오래전부터 스프링 연동을 지원하고있다. &lt;/span&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;span style=&quot;color: #222222;&quot;&gt;쿼츠는 아래 3가지 컴포넌트를 제공한다.&lt;/span&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;&lt;span style=&quot;color: #222222;&quot;&gt;1. 스케줄러(Scheduler)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222;&quot;&gt;스케줄러는 SchedulerFactory 를 통해서 가져올 수 있으며 JobDetails 및 트리거의 저장소 기능을 한다.&amp;nbsp;&lt;/span&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;span style=&quot;color: #222222;&quot;&gt;예시코드&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1644130741804&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public JobDetail buildJobDetail(Class job, String name, String group, Map params) {
    JobDataMap jobDataMap = new JobDataMap();
    jobDataMap.putAll(params);

    return newJob(job).withIdentity(name, group)
            .usingJobData(jobDataMap)
            .build();
}&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;span style=&quot;color: #222222;&quot;&gt;2. 잡(Job)&lt;/span&gt;&lt;/h4&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;span style=&quot;color: #222222;&quot;&gt;3. 트리거(Trigger)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;작업 실행 시점을 정의한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;트리거가 작동되어 쿼츠에게 잡을 실행하도록 지시하면, 잡의 개별 실행을 정의하는 JobDetails 객체가 생성된다.&lt;/span&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;span style=&quot;color: #222222;&quot;&gt;예시코드&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1644130888834&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Trigger buildJobTrigger(String scheduleExp) {
    return TriggerBuilder.newTrigger()
            .withSchedule(CronScheduleBuilder.cronSchedule(scheduleExp)).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;스프링 배치 작업&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;* 해당 실습은 스케줄러 시간에 따라 파일을 읽어들이고, api 통신하는 시간을 각각 나누어 진행한다.&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;- 파일 읽어들이기 (FileSchJob)&lt;/p&gt;
&lt;pre id=&quot;code_1644134419529&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.springbatch.project.scheduler;

import lombok.SneakyThrows;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.batch.core.*;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

// QuartzJobBean 을 사용하여 스프링 배치 잡을 기동하는 쿼츠 잡 작성

@Component
public class FileSchJob extends QuartzJobBean {

    @Autowired
    private Job fileJob;

    @Autowired
    private JobLauncher jobLauncher;

    @Autowired
    private JobExplorer jobExplorer; // jobRepository의 read 기능만 가지고 있음

    /**
     * 배치를 실행시키는 구문 : 스케줄링된 이벤트가 발생할때마다 한번씩 호출된다.
     * @param context
     * @throws JobExecutionException
     */
    @SneakyThrows
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {

        String requestDate = (String) context.getJobDetail().getJobDataMap().get(&quot;requestDate&quot;);

        JobParameters jobParameters = new JobParametersBuilder()
                .addLong(&quot;id&quot;, new Date().getTime())
                .addString(&quot;requestDate&quot;, requestDate)
                .toJobParameters();

        // 모든 job의 인스턴스 갯수를 가져올수 있음.
        int jobInstanceCount = jobExplorer.getJobInstanceCount(fileJob.getName());

        // 모든 job의 인스턴스 정보를 가져올수 있음. 0번째 부터 카운트 갯수까지
        List&amp;lt;JobInstance&amp;gt; jobInstances = jobExplorer.getJobInstances(fileJob.getName(), 0, jobInstanceCount);

        if(jobInstances.size() &amp;gt; 0) {
            for (JobInstance jobInstance : jobInstances) {
                // 여러개의 jobExecution을 가져온다.
                List&amp;lt;JobExecution&amp;gt; jobExecutions = jobExplorer.getJobExecutions(jobInstance);
                List&amp;lt;JobExecution&amp;gt; jobExecutionList = jobExecutions.stream()
                        .filter(jobExecution -&amp;gt; jobExecution.getJobParameters().getString(&quot;requestDate&quot;).equals(requestDate))
                        .collect(Collectors.toList());

                // 해당하는 날짜가 1개 이상인경우 배치를 실행하지 않는다.
                if(jobExecutionList.size() &amp;gt; 0) {
                    throw new JobExecutionException(requestDate + &quot; already exists&quot;);
                }
            }

        }

        jobLauncher.run(fileJob, jobParameters);
    }

}&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;- api 통신 (ApiSchJob)&lt;/p&gt;
&lt;pre id=&quot;code_1644134514156&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.springbatch.project.scheduler;

import lombok.SneakyThrows;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class ApiSchJob extends QuartzJobBean {

    @Autowired
    private Job apiJob;

    @Autowired
    private JobLauncher jobLauncher;

    /**
     * 배치를 실행시키는 구문 : 스케줄링된 이벤트가 발생할때마다 한번씩 호출된다.
     * @param context
     * @throws JobExecutionException
     */
    @SneakyThrows
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {

        JobParameters jobParameters = new JobParametersBuilder()
                .addLong(&quot;id&quot;, new Date().getTime())
                .toJobParameters();

        jobLauncher.run(apiJob, jobParameters);
    }
}&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;* api 스케줄러 작성&lt;/p&gt;
&lt;pre id=&quot;code_1644134815296&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.springbatch.project.scheduler;

import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

import static org.quartz.JobBuilder.newJob;

/**
 * 스케줄러 설정
 */
@Component
public class ApiJobRunner extends JobRunner {

    @Autowired
    private Scheduler scheduler;

    @Override
    protected void doRun(ApplicationArguments args) {

        JobDetail jobDetail = buildJobDetail(ApiSchJob.class, &quot;apiJob&quot;, &quot;batch&quot;, new HashMap());
        Trigger trigger = buildJobTrigger(&quot;0/10 * * * * ?&quot;); // 10초마다 실행

        try{
            scheduler.scheduleJob(jobDetail, trigger);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* file 스케줄러 작성&lt;/p&gt;
&lt;pre id=&quot;code_1644134866370&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.springbatch.project.scheduler;

import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.stereotype.Component;

import java.util.HashMap;

/**
 * 스케줄러 설정
 */
@Component
public class FileJobRunner extends JobRunner {

    @Autowired
    private Scheduler scheduler;

    @Override
    protected void doRun(ApplicationArguments args) {

        String[] sourceArgs = args.getSourceArgs();

        JobDetail jobDetail = buildJobDetail(FileSchJob.class, &quot;fileJob&quot;, &quot;batch&quot;, new HashMap());
        Trigger trigger = buildJobTrigger(&quot;0/30 * * * * ?&quot;); // 30초마다 실행
        jobDetail.getJobDataMap().put(&quot;requestDate&quot;, sourceArgs[0]);

        try{
            scheduler.scheduleJob(jobDetail, trigger);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

}&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;/p&gt;
&lt;pre id=&quot;code_1644134631694&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.springbatch.project.scheduler;

import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

import static org.quartz.JobBuilder.newJob;

/**
 * 공통 스케줄러 설정
 */
public abstract class JobRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {

        doRun(args);
    }

    public Trigger buildJobTrigger(String scheduleExp) {
        return TriggerBuilder.newTrigger()
                .withSchedule(CronScheduleBuilder.cronSchedule(scheduleExp)).build();
    }

    public JobDetail buildJobDetail(Class job, String name, String group, Map params) {
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.putAll(params);

        return newJob(job).withIdentity(name, group)
                .usingJobData(jobDataMap)
                .build();
    }

    protected abstract void doRun(ApplicationArguments args);

}&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1864&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7tdIV/btrsved0bP2/emMo9UqbBbjWakA5oz0eR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7tdIV/btrsved0bP2/emMo9UqbBbjWakA5oz0eR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7tdIV/btrsved0bP2/emMo9UqbBbjWakA5oz0eR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7tdIV%2Fbtrsved0bP2%2FemMo9UqbBbjWakA5oz0eR1%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;1864&quot; height=&quot;566&quot; data-origin-width=&quot;1864&quot; data-origin-height=&quot;566&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;왼쪽사진 file 스케줄러를 통해 파일이 생성되는 결과&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가운데사진 db적재된 결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽사진 api통신을 통해 적재된 결과&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://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링배치&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;해당 결과물은 git을 통해 확인 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/prodo-developer/spring-batch&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/prodo-developer/spring-batch&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>SPRING/개발 TIP</category>
      <author>prodo-developer</author>
      <guid isPermaLink="true">https://prodo-developer.tistory.com/174</guid>
      <comments>https://prodo-developer.tistory.com/174#entry174comment</comments>
      <pubDate>Sun, 6 Feb 2022 17:26:03 +0900</pubDate>
    </item>
    <item>
      <title>2021년을 돌아 보며 회고록</title>
      <link>https://prodo-developer.tistory.com/173</link>
      <description>&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;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;그만큼 열심히 살았다는 증거고 1년동안 있었던 일들을 몰아서 회고해본다.&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;어느덧 연차는 만 5년차, 나는 늘 고민이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SI회사를 다니면서 그리고 마지막으로 그토록 하고 싶었던 프리랜서까지 12월까지 마무리짓고,&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;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;부업으로 게임서버도 운영해보니 그동안 SI에서 끌려다녔던 느낌과 달리&amp;nbsp;직접 유저들과 소통을 하면서 유지보수를 하는과정에서 더 책임감과 애착심이 생기고, 잠을 설쳐도 피곤한줄 모르고&amp;nbsp;그렇게 2~3달은 지냈던거 같다. ⛱&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;파견업체에 다니면서 SI과 SM을 하면서 느꼈던점은 풀스택 같은 개념으로 프론트도 다루고, 백엔드도 다루지만&lt;/p&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;프론트는 뭔가 나랑 안맞는게 갈수록 느껴졌었고, 솔루션의 장단점은 확실히 편하게 개발하는건 좋으나, 트렌드를 쫓아가기엔 점점 거리감이 느껴지고, 이직을 하게&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;그 동안 내가 정말 성실하게 살았는가?? 내 자신을 돌아보니 쉬고 싶은거 다쉬고, 개발은 단지 컴공을 나왔으니까,&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;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;2. 이직 준비&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇부터 준비를 해야 할까, 그렇다면 누가들어도 알수있는 서비스회사로 한번 1년동안 준비해보고, 안되면 생계를 위해서라도 돌아가야지 라는 마음으로 올해 1월부터 결심하게 되었다.&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;서비스회사는 SI회사와 달리 보통 코딩테스트 또는 과제형식으로 진행된다.&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;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;1. 프로그래머스 : 코딩테스트와 실무 역량 모두 잡는 스터디: Java반 8기&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2111&quot; data-origin-height=&quot;897&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pWHLu/btrpdnFhNHM/IKVG4kJEx9nK0OY5n8UNR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pWHLu/btrpdnFhNHM/IKVG4kJEx9nK0OY5n8UNR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pWHLu/btrpdnFhNHM/IKVG4kJEx9nK0OY5n8UNR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpWHLu%2FbtrpdnFhNHM%2FIKVG4kJEx9nK0OY5n8UNR0%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;2111&quot; height=&quot;897&quot; data-origin-width=&quot;2111&quot; data-origin-height=&quot;897&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기수당 약 40명으로 이루어져 있으며, 지금은 오래되어 내역을 자세하게 볼 순 없지만, 주차별 미션이 주어지면&amp;nbsp; 과제는 깃으로 공유하며 코드리뷰도 꼼꼼하게&amp;nbsp;확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주 1회 온라인세션이 열리며, 담당자와 주차별 설명과 직접 소통할 수 있는 좋은 시간도 주어진다.&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;현재는 12기 진행중이며 모집중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://programmers.co.kr/learn/courses/13210&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://programmers.co.kr/learn/courses/13210&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;b&gt;2. 인프런 : &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%B0%94-%EA%B0%9C%EB%85%90/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;코딩테스트 전 꼭 알아야 할 개념과 문제(with 자바)&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%B0%94/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;정말 쉽게 풀어보는 코딩테스트 top 기본문제&lt;/b&gt;&lt;/a&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;1번으로만 준비하기에는 사실 부족한 부분이 있었다. 실시간 피드백이 다소 아쉬운부분이있어, 그런 부분은 내가 직접 채워 나가는수 밖에 없었다.&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;b&gt;&lt;u&gt;강의마다 질의응답이 가능&lt;/u&gt;&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 백엔드의 길&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1,2번은 기본적으로 갖춰야 할 소양이라면, 이제 내가 어디방향으로 갈지 방향을 잡아야 할 찰나에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우연히 인프런을 통해 김영한님의 JPA를 보게되고, 무료강의 및 미리보기를 보았는데,&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;785&quot; data-origin-height=&quot;871&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJJ7QV/btrpoyyi8b9/EOXATdiGvMIh2KE5H6kkz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJJ7QV/btrpoyyi8b9/EOXATdiGvMIh2KE5H6kkz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJJ7QV/btrpoyyi8b9/EOXATdiGvMIh2KE5H6kkz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJJ7QV%2Fbtrpoyyi8b9%2FEOXATdiGvMIh2KE5H6kkz1%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;785&quot; height=&quot;871&quot; data-origin-width=&quot;785&quot; data-origin-height=&quot;871&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M7CIw/btrpdodb4Ni/I9etzEo1RCwmLcKtV7LDQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M7CIw/btrpdodb4Ni/I9etzEo1RCwmLcKtV7LDQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M7CIw/btrpdodb4Ni/I9etzEo1RCwmLcKtV7LDQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM7CIw%2Fbtrpdodb4Ni%2FI9etzEo1RCwmLcKtV7LDQ1%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;757&quot; height=&quot;758&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에 투자한 돈만 대략 100만원은 넘는것같다. 내 인생에 이렇게 투자를 하게 될줄이야...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 백엔드의 길로 가고 싶다면 스프링, JPA 영한님 강의만 믿고 따라가도 면접,실무에서 막힘없이&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;이렇게 나는 인강을 모으는 재미와, 기술면접에 필요한 스터디를 열게 되어 부족했던 CS 지식들을 다시한번 정리하면서&lt;/p&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;1277&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E6P1O/btrpq8FTOFe/dbGD61RxPc3B1lOB8FKEb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E6P1O/btrpq8FTOFe/dbGD61RxPc3B1lOB8FKEb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E6P1O/btrpq8FTOFe/dbGD61RxPc3B1lOB8FKEb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE6P1O%2Fbtrpq8FTOFe%2FdbGD61RxPc3B1lOB8FKEb1%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;1277&quot; height=&quot;675&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;675&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;하지만 이걸로만 부족하다고 느껴, 사이드 프로젝트를 진행하게 되었고, 약 8개월동안 현재까지 많은 우여곡절이 있었으나, 이제 어느정도 정예멤버가 갖추어져 조금씩 진전해 나가고 있다.&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;a href=&quot;https://github.com/ecommerce-site-study&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/ecommerce-site-study&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1640875603694&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;profile&quot; data-og-title=&quot;ecommerce-site-study&quot; data-og-description=&quot;ecommerce-site-study has one repository available. Follow their code on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/ecommerce-site-study&quot; data-og-url=&quot;https://github.com/ecommerce-site-study&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bt37YT/hyMU0DSKgB/mQCypAPsgva57wNd4Nk9M0/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420&quot;&gt;&lt;a href=&quot;https://github.com/ecommerce-site-study&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/ecommerce-site-study&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bt37YT/hyMU0DSKgB/mQCypAPsgva57wNd4Nk9M0/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420');&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;ecommerce-site-study&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ecommerce-site-study has one repository available. Follow their code on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 취업성공 그리고 도전해야 될 큰 산&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;/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;그 간절함이 운좋게도, 6개월만에 기회가 오게되어 현재는 인터파크에 입사하게되어,&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;최종합격과 동시에 준비하고 있었던 넥스트스텝의 우아한 테크캠프 2기까지 선발되어,&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqawUW/btrpmgkqat2/FmlpOyBoeaSCU2J54PZLfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqawUW/btrpmgkqat2/FmlpOyBoeaSCU2J54PZLfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqawUW/btrpmgkqat2/FmlpOyBoeaSCU2J54PZLfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqawUW%2Fbtrpmgkqat2%2FFmlpOyBoeaSCU2J54PZLfk%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;933&quot; height=&quot;592&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;592&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;그러나, 이게 두가지를 병행하기엔 정말 높은 산이었다.&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;/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;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;캡틴 포비님께서도 말씀해주셨지만 캠프기간 2달동안은 나 자신과의 약속을 지켜야만 이 진도를 따라갈수 있다고하셨다.&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;1. 주변 친구들과 잠시 안녕&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 전자매체 안녕&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 퇴근 후, 주말에도 온전히 이곳에 집중하기&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;정말 이 세가지를 지켜야만 진도를 쫓아갈 수 있었다. 그 만큼 8주과정 동안 과제가 수없이 쏟아지며, 고생하는 만큼&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. TDD를 통한 리팩토링&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 동기들과 함께 페어프로그래밍&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. JPA 실습&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. ATDD, DDD 경험&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. AWS를 통한 인프라 실습&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 도커 컨테이너를 이용한 실습&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 부하테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. 레거시 코드 리팩토링&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9. Redis와 mysql를 통한 최적화 만들기&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;자세한 썰들은 회고록 목록에 주차별 느낀점을 따로 정리해 두었다.&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;우테캠을 희망하는 사람이 있다면, 이 많은것들을 소화하기 위해 2달동안은 정말 각오하고 들어올것...&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 본업에 집중&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에 입사한지 어느덧 8개월차가 되었다.&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;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;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;이미 개발이 어느정도 되어있고, 되어있는 서비스를 운영하는것이 쉬운것만은 아니었다. 그래서 더욱 더 작업을 할 때마다 더 조심스럽고, 더 꼼꼼해지려고 한다.&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;SI처럼 만들고 철수하는것이 아닌 직접 서비스를 운영하는 입장에서 퇴근하고 나서도, 주말에서도 계속 생각나는것이&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;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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 2022 목표&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;b&gt;- 본업에 더 충실하기&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;- 인강 정주행하기&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;- 전공 독서하기&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 스프링부트와 AWS로 혼자 구현하는 웹 서비스&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;p data-ke-size=&quot;size16&quot;&gt;* Real Mysql&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;p data-ke-size=&quot;size16&quot;&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;- 사이드 프로젝트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2021년도에는 마음맞는 팀원들과 알아가는 기간이 였다면, 올해는 조금 더 퀄리티 좋은 코드와, 꾸준한 관심의 결과물로&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;- 건강유지 (운동 생활화)&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;체력이 뒷받침 되어야 어떠한 상황을 겪어도 버틸 수 있게 해주는 원동력인거 같다.&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;p data-ke-size=&quot;size16&quot;&gt;평일, 주말 가리지 않고 웨이트 운동을 통해 특별한 일이 없으면 헬스장으로 출근하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;못가는 날에는 집에서 홈트라도 진행할 것.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. 마무리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12월 31일. 한해를 마무리 하면서 꼭 작성하고 싶었던 회고록을 작성하게 되어 기쁘고,&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;사이드 프로젝트와, 개인공부를 통한 Git에 1일 1커밋을 통해 성실함을 계속 보여주고 앞으로도 그렇게 되길&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;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>오늘의 일상/회고록</category>
      <author>prodo-developer</author>
      <guid isPermaLink="true">https://prodo-developer.tistory.com/173</guid>
      <comments>https://prodo-developer.tistory.com/173#entry173comment</comments>
      <pubDate>Fri, 31 Dec 2021 00:22:38 +0900</pubDate>
    </item>
    <item>
      <title>[SPRING] 스프링 배치  테이블 초기화 방법</title>
      <link>https://prodo-developer.tistory.com/172</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 초기화를 돌리고 다음과 같은 이슈를 발생하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1639497207453&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class jobBatchResetTest {
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job job() {
        return jobBuilderFactory.get(&quot;batchJob1&quot;)
                .incrementer(new RunIdIncrementer()) // 동일 파라미터인데 다시 실행하고 싶을때 사용하라는 의미로 RunIdIncrementer를 제공
                .start(step1())
                .next(step2())
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get(&quot;step1&quot;)
                .tasklet(new Tasklet() {
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                        System.out.println(&quot;step1 was executed&quot;);
                        return RepeatStatus.FINISHED;
                    }
                })
                .build();
    }

    @Bean
    public Step step2() {
        return stepBuilderFactory.get(&quot;step2&quot;)
                .tasklet(new Tasklet() {
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                        System.out.println(&quot;step2 was executed&quot;);
                        return RepeatStatus.FINISHED;
                    }
                })
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000;&quot;&gt;org.springframework.dao.DuplicateKeyException: PreparedStatementCallback; SQL [INSERT into BATCH_STEP_EXECUTION(STEP_EXECUTION_ID, VERSION, STEP_NAME, JOB_EXECUTION_ID, START_TIME, END_TIME, STATUS, COMMIT_COUNT, READ_COUNT, FILTER_COUNT, WRITE_COUNT, EXIT_CODE, EXIT_MESSAGE, READ_SKIP_COUNT, WRITE_SKIP_COUNT, PROCESS_SKIP_COUNT, ROLLBACK_COUNT, LAST_UPDATED) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)]; Duplicate entry '0' for key 'BATCH_STEP_EXECUTION.PRIMARY'; nested exception is java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '0' for key 'BATCH_STEP_EXECUTION.PRIMARY'&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Caused by: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '0' for key 'BATCH_STEP_EXECUTION.PRIMARY'&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개선책으로는 아래와 같이 해봤는데도 실패....&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. @SpringBootApplication 주석 후 아래와 같이 진행&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000;&quot;&gt;@EnableAutoConfiguration(exclude={BatchAutoConfiguration.class})&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. @SpringBootApplication(exclude={BatchAutoConfiguration.class})&lt;/span&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;&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_1639497285925&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;delete from BATCH_JOB_EXECUTION_CONTEXT;
delete from BATCH_JOB_EXECUTION_PARAMS;
delete from BATCH_JOB_EXECUTION_SEQ;
delete from BATCH_JOB_SEQ;
delete from BATCH_STEP_EXECUTION_CONTEXT;
delete from BATCH_STEP_EXECUTION_SEQ;
delete from BATCH_STEP_EXECUTION;
delete from BATCH_JOB_EXECUTION;
delete from BATCH_JOB_INSTANCE;

INSERT INTO BATCH_STEP_EXECUTION_SEQ values(0, '0');
INSERT INTO BATCH_JOB_EXECUTION_SEQ values(0, '0');
INSERT INTO BATCH_JOB_SEQ values(0, '0');&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;※ 시퀀스를 수동으로 넣어야 하는 DB 일 경우 위와 같이 삭제했을 경우 다시 입력해 주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Postgresql 같은 경우 위와 같은 처리가 필요하지 않습니다.&lt;/p&gt;</description>
      <category>SPRING/기본 상식</category>
      <author>prodo-developer</author>
      <guid isPermaLink="true">https://prodo-developer.tistory.com/172</guid>
      <comments>https://prodo-developer.tistory.com/172#entry172comment</comments>
      <pubDate>Wed, 15 Dec 2021 00:55:05 +0900</pubDate>
    </item>
    <item>
      <title>깃허브(github) 잔디심기 안될 때</title>
      <link>https://prodo-developer.tistory.com/171</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 노트북을 변경하고, 환경셋팅을 하고 여전히 1일 1커밋을 진행 하면 잔디가 심어져야 하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속 되지않아서 브랜치를 지웠다가 해보기도하고, 권한도 바꿔보고 별의 별짓을 약 1주일간 했던거같다.&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;원인은 git을 공유할 때 이메일주소를 이상하게 적었다. (오타  )&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1) 깃허브 계정의 이메일 주소 확인하기&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깃허브 프로필 클릭 -&amp;gt; settings -&amp;gt; 이메일 주소 확인&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;211&quot; data-origin-height=&quot;551&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd0OZB/btrfhlbNhrT/4RkWdSQOpS4xnoiygjVjR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd0OZB/btrfhlbNhrT/4RkWdSQOpS4xnoiygjVjR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd0OZB/btrfhlbNhrT/4RkWdSQOpS4xnoiygjVjR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd0OZB%2FbtrfhlbNhrT%2F4RkWdSQOpS4xnoiygjVjR0%2Fimg.png&quot; data-origin-width=&quot;211&quot; data-origin-height=&quot;551&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;499&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFSYo3/btrfhzU97KV/2mpoDkwAuD4KPPuP54bZl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFSYo3/btrfhzU97KV/2mpoDkwAuD4KPPuP54bZl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFSYo3/btrfhzU97KV/2mpoDkwAuD4KPPuP54bZl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFSYo3%2FbtrfhzU97KV%2F2mpoDkwAuD4KPPuP54bZl0%2Fimg.png&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;499&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2) 명령어로 config list 확인하기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1631795945252&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; padding: 15px; overflow: auto; font-size: 14px; line-height: 27px; background: #f6f7f8; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: block; color: #383a42; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git config --global --list&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;config 에 이메일주소가 저장이 안되어있거나, 오타 또는 {중괄호가 씌워진 값이면 당장 지울것}&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1631795945252&quot; class=&quot;javascript&quot; style=&quot;padding: 13px 20px !important; overflow: auto; font-size: 14px; line-height: 27px; background-color: #f8f8f8; color: #303030; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; margin: 0px 0px 20px !important 0px;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//이메일변경
git config --global user.email &quot;aaa@a.com&quot;

//유저이름 변경
git config --global user.name &quot;USER_NAME&quot;&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;위와 같이 변경 후 새로운 commit 거리를 만들고 push 하면 정상적으로 잔디심기 성공!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기타 TIP</category>
      <author>prodo-developer</author>
      <guid isPermaLink="true">https://prodo-developer.tistory.com/171</guid>
      <comments>https://prodo-developer.tistory.com/171#entry171comment</comments>
      <pubDate>Thu, 16 Sep 2021 21:43:30 +0900</pubDate>
    </item>
    <item>
      <title>[SPRING] 주문금액 집계 프로젝트 실습 part3 (성능개선과 성능비교)</title>
      <link>https://prodo-developer.tistory.com/170</link>
      <description>&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;SaveUserTasklet에서&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; User 30,000건 &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;저장&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;, Chunk &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Size는&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; 1,000&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;성능&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;개선&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;대상&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Step은&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;userLevelUpStep&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;아래&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; 표 &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;순서대로&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;실행&lt;/span&gt;&lt;/p&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;3번 씩 실행&lt;/li&gt;
&lt;li&gt;PC 환경에 따라 성능이 다를 수 있음 (해당 개인PC는 강의에 나온 PC보다 느렸음  )&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; width=&quot;962&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width=&quot;248&quot; height=&quot;40&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1회&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td width=&quot;248&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2회&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3회&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width=&quot;248&quot; height=&quot;40&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Simple Step&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;15557mills&lt;/td&gt;
&lt;td width=&quot;248&quot;&gt;15081mills&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;14628mills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width=&quot;248&quot; height=&quot;40&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Async Step&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;15074mills&lt;/td&gt;
&lt;td width=&quot;248&quot;&gt;15511mills&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;14940mills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width=&quot;248&quot; height=&quot;40&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Multi-Thread Step(가장빠름)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;9038mills&lt;/td&gt;
&lt;td width=&quot;248&quot;&gt;8943mills&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;9909mills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width=&quot;248&quot; height=&quot;40&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Partition Step&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;9659mills&lt;/td&gt;
&lt;td width=&quot;248&quot;&gt;10890mills&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;10629mills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width=&quot;248&quot; height=&quot;40&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Async + Partition Step&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;10795mills&lt;/td&gt;
&lt;td width=&quot;248&quot;&gt;10919mills&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;11186mills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width=&quot;248&quot; height=&quot;40&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Parallel Step&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;14236mills&lt;/td&gt;
&lt;td width=&quot;248&quot;&gt;14433mills&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;14377mills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td width=&quot;248&quot; height=&quot;40&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Partition + &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Parallel Step&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;10512mills&lt;/td&gt;
&lt;td width=&quot;248&quot;&gt;10294mills&lt;/td&gt;
&lt;td width=&quot;233&quot;&gt;10252mills&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 4차부터는 큰 속도의 변화를 찾아볼 수 없었다. 15403mills, 15024mils, 이후에도... 그래서 3번씩만 진행하기로 했다.&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;span style=&quot;color: #000000;&quot;&gt;Async Step을 적용하기 위해서는 &lt;span style=&quot;color: #000000;&quot;&gt;Async 관련된 라이브러리를 추가해야 합니다.&lt;/span&gt;&lt;/span&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;1. java.util.concurrent에서 제공되는 Future 기반 asynchronous Async를 사용하기 위해 spring-batch-integration 필요.&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;2. build.gradle 파일에서 implementation 'org.springframework.batch:spring-batch-integration' 추가&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;span style=&quot;color: #000000;&quot;&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;ItemProcessor와&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;ItemWriter를&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Async로&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;실행하도록 수정합니다.&lt;/span&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;span style=&quot;color: #595959;&quot;&gt;4. Async&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Step은&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;ItemProcessor와&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;ItemWriter&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;기준으로&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;비동기&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;처리 합니다.&lt;/span&gt;&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-origin-width=&quot;1049&quot; data-origin-height=&quot;364&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWPLpN/btrbCtxTj2v/ZWt3JAHUkPLJdgW94dHXdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWPLpN/btrbCtxTj2v/ZWt3JAHUkPLJdgW94dHXdk/img.png&quot; data-alt=&quot;Single Thread에서 asynchronous Async로 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWPLpN/btrbCtxTj2v/ZWt3JAHUkPLJdgW94dHXdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWPLpN%2FbtrbCtxTj2v%2FZWt3JAHUkPLJdgW94dHXdk%2Fimg.png&quot; data-origin-width=&quot;1049&quot; data-origin-height=&quot;364&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;Single Thread에서 asynchronous Async로 변경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AsyncStep 기능 작업&lt;/p&gt;
&lt;pre id=&quot;code_1628514979294&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@Slf4j
public class AsyncUserConfiguration {

    private final String JOB_NAME = &quot;asyncUserJob&quot;;
    public static final int CHUNK_SIZE = 1000;
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final UserRepository userRepository;
    private final EntityManagerFactory entityManagerFactory;
    private final DataSource dataSource;
    private final TaskExecutor taskExecutor;

    public AsyncUserConfiguration(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, UserRepository userRepository, EntityManagerFactory entityManagerFactory, DataSource dataSource, TaskExecutor taskExecutor) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
        this.userRepository = userRepository;
        this.entityManagerFactory = entityManagerFactory;
        this.dataSource = dataSource;
        this.taskExecutor = taskExecutor;
    }

    @Bean(JOB_NAME)
    public Job userJob() throws Exception {
        return this.jobBuilderFactory.get(JOB_NAME)
                .incrementer(new RunIdIncrementer())
                .start(this.saveUserStep())
                .next(this.userLevelUpStep())
                .listener(new LevelUpJobExecutionListener(userRepository))
                .next(new JobParameterDecide(&quot;date&quot;)) // 가져온값이 아래의 CONTINUE인지 체크
                .on(JobParameterDecide.CONTINUE.getName()) // CONTINUE이면 아래의 to메서드 실행
                .to(this.orderStatisticsStep(null))
                .build()
                .build();
    }

    @Bean(JOB_NAME + &quot;_orderStatisticsStep&quot;)
    @JobScope
    public Step orderStatisticsStep(@Value(&quot;#{jobParameters[date]}&quot;) String date) throws Exception {
        return this.stepBuilderFactory.get(JOB_NAME + &quot;_orderStatisticsStep&quot;)
                .&amp;lt;OrderStatistics, OrderStatistics&amp;gt;chunk(CHUNK_SIZE)
                .reader(orderStatisticsItemReader(date))
                .writer(orderStatisticsItemWriter(date))
                .build();
    }

    private ItemReader&amp;lt;? extends OrderStatistics&amp;gt; orderStatisticsItemReader(String date) throws Exception {
        YearMonth yearMonth = YearMonth.parse(date);

        Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;startDate&quot;, yearMonth.atDay(1)); // 2021년 8월 1일
        parameters.put(&quot;endDate&quot;, yearMonth.atEndOfMonth()); // 8월의 마지막 날

        Map&amp;lt;String, Order&amp;gt; sortKey = new HashMap&amp;lt;&amp;gt;();
        sortKey.put(&quot;created_date&quot;, Order.ASCENDING);

        JdbcPagingItemReader&amp;lt;OrderStatistics&amp;gt; itemReader = new JdbcPagingItemReaderBuilder&amp;lt;OrderStatistics&amp;gt;()
                .dataSource(this.dataSource)
                .rowMapper((resultSet, i) -&amp;gt; OrderStatistics.builder()
                        .amount(resultSet.getString(1))
                        .date(LocalDate.parse(resultSet.getString(2), DateTimeFormatter.ISO_DATE))
                        .build())
                .pageSize(CHUNK_SIZE) // 페이징 설정
                .name(JOB_NAME + &quot;_orderStatisticsItemReader&quot;)
                .selectClause(&quot;sum(amount), created_date&quot;)
                .fromClause(&quot;orders&quot;)
                .whereClause(&quot;created_date &amp;gt;= :startDate and created_date &amp;lt;= :endDate&quot;)
                .groupClause(&quot;created_date&quot;)
                .parameterValues(parameters)
                .sortKeys(sortKey)
                .build();

        itemReader.afterPropertiesSet();

        return itemReader;
    }

    private ItemWriter&amp;lt;? super OrderStatistics&amp;gt; orderStatisticsItemWriter(String date) throws Exception {
        YearMonth yearMonth = YearMonth.parse(date);

        String fileName = yearMonth.getYear() + &quot;년_&quot; + yearMonth.getMonthValue() + &quot;월_일별_주문_금액.csv&quot;;

        BeanWrapperFieldExtractor&amp;lt;OrderStatistics&amp;gt; fieldExtractor = new BeanWrapperFieldExtractor&amp;lt;&amp;gt;();
        fieldExtractor.setNames(new String[]{&quot;amount&quot;, &quot;date&quot;});

        DelimitedLineAggregator&amp;lt;OrderStatistics&amp;gt; lineAggregator = new DelimitedLineAggregator&amp;lt;&amp;gt;();
        lineAggregator.setDelimiter(&quot;,&quot;);
        lineAggregator.setFieldExtractor(fieldExtractor);

        FlatFileItemWriter&amp;lt;OrderStatistics&amp;gt; itemWriter = new FlatFileItemWriterBuilder&amp;lt;OrderStatistics&amp;gt;()
                .resource(new FileSystemResource(&quot;output/&quot; + fileName))
                .lineAggregator(lineAggregator)
                .name(JOB_NAME + &quot;_orderStatisticsItemWriter&quot;)
                .encoding(&quot;UTF-8&quot;)
                .headerCallback(writer -&amp;gt; writer.write(&quot;total_amount,date&quot;))
                .build();

        itemWriter.afterPropertiesSet();

        return itemWriter;
    }

    @Bean(JOB_NAME + &quot;_saveUserStep&quot;)
    public Step saveUserStep() {
        return this.stepBuilderFactory.get(JOB_NAME + &quot;_saveUserStep&quot;)
                .tasklet(new SaveUserTasklet(userRepository))
                .build();
    }

    @Bean(JOB_NAME + &quot;_userLevelUpStep&quot;)
    public Step userLevelUpStep() throws Exception {
        return this.stepBuilderFactory.get(JOB_NAME + &quot;_userLevelUpStep&quot;)
                .&amp;lt;User, Future&amp;lt;User&amp;gt;&amp;gt;chunk(CHUNK_SIZE) // Future로 감싸야 컴파일 해결 됨.
                .reader(itemReader())
                .processor(itemProcessor())
                .writer(itemWriter())
                .build();
    }

    private AsyncItemWriter&amp;lt;User&amp;gt; itemWriter() {
        ItemWriter&amp;lt;User&amp;gt; itemWriter = users -&amp;gt; users.forEach(x -&amp;gt; {
                x.levelUp();
                userRepository.save(x);
        });

        AsyncItemWriter&amp;lt;User&amp;gt; asyncItemWriter = new AsyncItemWriter&amp;lt;&amp;gt;();
        asyncItemWriter.setDelegate(itemWriter);

        return asyncItemWriter;
    }

    private AsyncItemProcessor&amp;lt;User, User&amp;gt; itemProcessor() {
        ItemProcessor&amp;lt;User, User&amp;gt; itemProcessor = user -&amp;gt; {
            if (user.availableLeveUp()) {
                return user;
            }

            return null;
        };

        AsyncItemProcessor&amp;lt;User, User&amp;gt; asyncItemProcessor = new AsyncItemProcessor&amp;lt;&amp;gt;();
        asyncItemProcessor.setDelegate(itemProcessor);
        asyncItemProcessor.setTaskExecutor(this.taskExecutor);

        return asyncItemProcessor;
    }

    private ItemReader&amp;lt;? extends User&amp;gt; itemReader() throws Exception {
        JpaPagingItemReader&amp;lt;User&amp;gt; itemReader = new JpaPagingItemReaderBuilder&amp;lt;User&amp;gt;()
                .queryString(&quot;select u from User u&quot;)
                .entityManagerFactory(entityManagerFactory)
                .pageSize(CHUNK_SIZE)
                .name(JOB_NAME + &quot;_userItemReader&quot;)
                .build();

        itemReader.afterPropertiesSet();

        return itemReader;
    }
}&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;span style=&quot;color: #000000;&quot;&gt;Async Step은 &lt;span style=&quot;color: #000000;&quot;&gt;Simple Step보다 속도가 조금 더 개선되는 줄 알고 기대했다가 큰 차이가 없었다.. ㅠㅠ&lt;/span&gt;&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다음은 가장 빨랐던 &lt;b&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Multi-Thread Step&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 적용해보자.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&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;span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Multi-Thread &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Step은&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; Chunk &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;단위로&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;멀티&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;스레딩&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;처리&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Thread-Safe 한 &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;ItemReader&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;필수&lt;/span&gt;&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 alignLeft&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;389&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xsh17/btrbwg67M1U/13nlTvCYFZSoeozKoGqTKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xsh17/btrbwg67M1U/13nlTvCYFZSoeozKoGqTKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xsh17/btrbwg67M1U/13nlTvCYFZSoeozKoGqTKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxsh17%2Fbtrbwg67M1U%2F13nlTvCYFZSoeozKoGqTKk%2Fimg.png&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;389&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SpringBatchExampleApplication.java (메인메서드 자동 종료 및 TaskExecutor기반 쓰레드 설정)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1628515351038&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication
@EnableBatchProcessing
public class SpringBatchExampleApplication {

	public static void main(String[] args) {
		// 스프링 배치가 정상적으로 종료될 수 있도록 System.exit(SpringApplication.exit();
		System.exit(SpringApplication.exit(SpringApplication.run(SpringBatchExampleApplication.class, args)));
	}

	/**
	 * // TaskExecutor가 기본적으로 Bean으로 생성되어 있기 때문에 기본 @Bean으로 사용하기 위함을 표시
	 */
	@Bean
	@Primary
	TaskExecutor taskExecutor() {
		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
		taskExecutor.setCorePoolSize(10); // 쓰레드 기본 사이즈 10
		taskExecutor.setMaxPoolSize(20); // 최대 쓰레드 사이즈 20
		taskExecutor.setThreadNamePrefix(&quot;batch-thread-&quot;); // 배치 쓰레드 Prefix 이름이 찍히게 됨.
		taskExecutor.initialize();
		return taskExecutor;
	}
}&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;User.java 수정 (FetchType.EAGER 변경)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1669&quot; data-origin-height=&quot;617&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byXiKk/btrbJcn7Xwx/QGpSUqCku3mgqo4Po4sPJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byXiKk/btrbJcn7Xwx/QGpSUqCku3mgqo4Po4sPJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byXiKk/btrbJcn7Xwx/QGpSUqCku3mgqo4Po4sPJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyXiKk%2FbtrbJcn7Xwx%2FQGpSUqCku3mgqo4Po4sPJ0%2Fimg.png&quot; data-origin-width=&quot;1669&quot; data-origin-height=&quot;617&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Multi-Thread Step 기능 작업&lt;/p&gt;
&lt;pre id=&quot;code_1628515599244&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@Slf4j
public class MultiThreadUserConfiguration {

    private final String JOB_NAME = &quot;multiThreadUserJob&quot;;
    public static final int CHUNK_SIZE = 1000;
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final UserRepository userRepository;
    private final EntityManagerFactory entityManagerFactory;
    private final DataSource dataSource;
    private final TaskExecutor taskExecutor;

    public MultiThreadUserConfiguration(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, UserRepository userRepository, EntityManagerFactory entityManagerFactory, DataSource dataSource, TaskExecutor taskExecutor) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
        this.userRepository = userRepository;
        this.entityManagerFactory = entityManagerFactory;
        this.dataSource = dataSource;
        this.taskExecutor = taskExecutor;
    }

    @Bean(JOB_NAME)
    public Job userJob() throws Exception {
        return this.jobBuilderFactory.get(JOB_NAME)
                .incrementer(new RunIdIncrementer())
                .start(this.saveUserStep())
                .next(this.userLevelUpStep())
                .listener(new LevelUpJobExecutionListener(userRepository))
                .next(new JobParameterDecide(&quot;date&quot;)) // 가져온값이 아래의 CONTINUE인지 체크
                .on(JobParameterDecide.CONTINUE.getName()) // CONTINUE이면 아래의 to메서드 실행
                .to(this.orderStatisticsStep(null))
                .build()
                .build();
    }

    @Bean(JOB_NAME + &quot;_orderStatisticsStep&quot;)
    @JobScope
    public Step orderStatisticsStep(@Value(&quot;#{jobParameters[date]}&quot;) String date) throws Exception {
        return this.stepBuilderFactory.get(JOB_NAME + &quot;_orderStatisticsStep&quot;)
                .&amp;lt;OrderStatistics, OrderStatistics&amp;gt;chunk(CHUNK_SIZE)
                .reader(orderStatisticsItemReader(date))
                .writer(orderStatisticsItemWriter(date))
                .build();
    }

    private ItemReader&amp;lt;? extends OrderStatistics&amp;gt; orderStatisticsItemReader(String date) throws Exception {
        YearMonth yearMonth = YearMonth.parse(date);

        Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;startDate&quot;, yearMonth.atDay(1)); // 2021년 8월 1일
        parameters.put(&quot;endDate&quot;, yearMonth.atEndOfMonth()); // 8월의 마지막 날

        Map&amp;lt;String, Order&amp;gt; sortKey = new HashMap&amp;lt;&amp;gt;();
        sortKey.put(&quot;created_date&quot;, Order.ASCENDING);

        JdbcPagingItemReader&amp;lt;OrderStatistics&amp;gt; itemReader = new JdbcPagingItemReaderBuilder&amp;lt;OrderStatistics&amp;gt;()
                .dataSource(this.dataSource)
                .rowMapper((resultSet, i) -&amp;gt; OrderStatistics.builder()
                        .amount(resultSet.getString(1))
                        .date(LocalDate.parse(resultSet.getString(2), DateTimeFormatter.ISO_DATE))
                        .build())
                .pageSize(CHUNK_SIZE) // 페이징 설정
                .name(JOB_NAME + &quot;_orderStatisticsItemReader&quot;)
                .selectClause(&quot;sum(amount), created_date&quot;)
                .fromClause(&quot;orders&quot;)
                .whereClause(&quot;created_date &amp;gt;= :startDate and created_date &amp;lt;= :endDate&quot;)
                .groupClause(&quot;created_date&quot;)
                .parameterValues(parameters)
                .sortKeys(sortKey)
                .build();

        itemReader.afterPropertiesSet();

        return itemReader;
    }

    private ItemWriter&amp;lt;? super OrderStatistics&amp;gt; orderStatisticsItemWriter(String date) throws Exception {
        YearMonth yearMonth = YearMonth.parse(date);

        String fileName = yearMonth.getYear() + &quot;년_&quot; + yearMonth.getMonthValue() + &quot;월_일별_주문_금액.csv&quot;;

        BeanWrapperFieldExtractor&amp;lt;OrderStatistics&amp;gt; fieldExtractor = new BeanWrapperFieldExtractor&amp;lt;&amp;gt;();
        fieldExtractor.setNames(new String[]{&quot;amount&quot;, &quot;date&quot;});

        DelimitedLineAggregator&amp;lt;OrderStatistics&amp;gt; lineAggregator = new DelimitedLineAggregator&amp;lt;&amp;gt;();
        lineAggregator.setDelimiter(&quot;,&quot;);
        lineAggregator.setFieldExtractor(fieldExtractor);

        FlatFileItemWriter&amp;lt;OrderStatistics&amp;gt; itemWriter = new FlatFileItemWriterBuilder&amp;lt;OrderStatistics&amp;gt;()
                .resource(new FileSystemResource(&quot;output/&quot; + fileName))
                .lineAggregator(lineAggregator)
                .name(JOB_NAME + &quot;_orderStatisticsItemWriter&quot;)
                .encoding(&quot;UTF-8&quot;)
                .headerCallback(writer -&amp;gt; writer.write(&quot;total_amount,date&quot;))
                .build();

        itemWriter.afterPropertiesSet();

        return itemWriter;
    }

    @Bean(JOB_NAME + &quot;_saveUserStep&quot;)
    public Step saveUserStep() {
        return this.stepBuilderFactory.get(JOB_NAME + &quot;_saveUserStep&quot;)
                .tasklet(new SaveUserTasklet(userRepository))
                .build();
    }

    @Bean(JOB_NAME + &quot;_userLevelUpStep&quot;)
    public Step userLevelUpStep() throws Exception {
        return this.stepBuilderFactory.get(JOB_NAME + &quot;_userLevelUpStep&quot;)
                .&amp;lt;User, User&amp;gt;chunk(CHUNK_SIZE)
                .reader(itemReader())
                .processor(itemProcessor())
                .writer(itemWriter())
                .taskExecutor(this.taskExecutor) // 생성자로 주입받은 taskExecutor 사용
                .throttleLimit(8) // 몇개의 쓰레드로 처리할것인지 (기본 4개)
                .build();
    }

    private ItemWriter&amp;lt;? super User&amp;gt; itemWriter() {
        return users -&amp;gt; users.forEach(x -&amp;gt; {
                x.levelUp();
                userRepository.save(x);
        });
    }

    private ItemProcessor&amp;lt;? super User, ? extends User&amp;gt; itemProcessor() {
        return user -&amp;gt; {
            if (user.availableLeveUp()) {
                return user;
            }

            return null;
        };
    }

    private ItemReader&amp;lt;? extends User&amp;gt; itemReader() throws Exception {
        JpaPagingItemReader&amp;lt;User&amp;gt; itemReader = new JpaPagingItemReaderBuilder&amp;lt;User&amp;gt;()
                .queryString(&quot;select u from User u&quot;)
                .entityManagerFactory(entityManagerFactory)
                .pageSize(CHUNK_SIZE)
                .name(JOB_NAME + &quot;_userItemReader&quot;)
                .build();

        itemReader.afterPropertiesSet();

        return itemReader;
    }
}&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;Partition Step은 어떨까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;하나의&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; Master &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;기준으로&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;여러&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; Slave &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Step을&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;생성해&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; Step &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;기준으로&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; Multi-Thread &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;처리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;예를&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;들어&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;item이 40,000개, Slave Step이 8개면&lt;/li&gt;
&lt;li&gt;40000 / 8 = 5000 이므로 하나의 Slave Step 당 5,000건 씩 나눠서 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Slave &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Step은&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;각각&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;하나의&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Step으로&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;동작&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;323&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZVIVJ/btrbwgzh7ac/wsy83I6ha1mnHIUp5JAGVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZVIVJ/btrbwgzh7ac/wsy83I6ha1mnHIUp5JAGVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZVIVJ/btrbwgzh7ac/wsy83I6ha1mnHIUp5JAGVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZVIVJ%2Fbtrbwgzh7ac%2Fwsy83I6ha1mnHIUp5JAGVK%2Fimg.png&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;323&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&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;Partitioner 기능 추가&lt;/p&gt;
&lt;pre id=&quot;code_1628515980749&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@Slf4j
public class PartitionUserConfiguration {

    private final String JOB_NAME = &quot;partitionUserJob&quot;;
    public static final int CHUNK_SIZE = 1000;
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final UserRepository userRepository;
    private final EntityManagerFactory entityManagerFactory;
    private final DataSource dataSource;
    private final TaskExecutor taskExecutor;

    public PartitionUserConfiguration(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, UserRepository userRepository, EntityManagerFactory entityManagerFactory, DataSource dataSource, TaskExecutor taskExecutor) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
        this.userRepository = userRepository;
        this.entityManagerFactory = entityManagerFactory;
        this.dataSource = dataSource;
        this.taskExecutor = taskExecutor;
    }

    @Bean(JOB_NAME)
    public Job userJob() throws Exception {
        return this.jobBuilderFactory.get(JOB_NAME)
                .incrementer(new RunIdIncrementer())
                .start(this.saveUserStep())
                .next(this.userLevelUpManagerStep())
                .listener(new LevelUpJobExecutionListener(userRepository))
                .next(new JobParameterDecide(&quot;date&quot;)) // 가져온값이 아래의 CONTINUE인지 체크
                .on(JobParameterDecide.CONTINUE.getName()) // CONTINUE이면 아래의 to메서드 실행
                .to(this.orderStatisticsStep(null))
                .build()
                .build();
    }

    @Bean(JOB_NAME + &quot;_orderStatisticsStep&quot;)
    @JobScope
    public Step orderStatisticsStep(@Value(&quot;#{jobParameters[date]}&quot;) String date) throws Exception {
        return this.stepBuilderFactory.get(JOB_NAME + &quot;_orderStatisticsStep&quot;)
                .&amp;lt;OrderStatistics, OrderStatistics&amp;gt;chunk(CHUNK_SIZE)
                .reader(orderStatisticsItemReader(date))
                .writer(orderStatisticsItemWriter(date))
                .build();
    }

    private ItemReader&amp;lt;? extends OrderStatistics&amp;gt; orderStatisticsItemReader(String date) throws Exception {
        YearMonth yearMonth = YearMonth.parse(date);

        Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;startDate&quot;, yearMonth.atDay(1)); // 2021년 8월 1일
        parameters.put(&quot;endDate&quot;, yearMonth.atEndOfMonth()); // 8월의 마지막 날

        Map&amp;lt;String, Order&amp;gt; sortKey = new HashMap&amp;lt;&amp;gt;();
        sortKey.put(&quot;created_date&quot;, Order.ASCENDING);

        JdbcPagingItemReader&amp;lt;OrderStatistics&amp;gt; itemReader = new JdbcPagingItemReaderBuilder&amp;lt;OrderStatistics&amp;gt;()
                .dataSource(this.dataSource)
                .rowMapper((resultSet, i) -&amp;gt; OrderStatistics.builder()
                        .amount(resultSet.getString(1))
                        .date(LocalDate.parse(resultSet.getString(2), DateTimeFormatter.ISO_DATE))
                        .build())
                .pageSize(CHUNK_SIZE) // 페이징 설정
                .name(JOB_NAME + &quot;_orderStatisticsItemReader&quot;)
                .selectClause(&quot;sum(amount), created_date&quot;)
                .fromClause(&quot;orders&quot;)
                .whereClause(&quot;created_date &amp;gt;= :startDate and created_date &amp;lt;= :endDate&quot;)
                .groupClause(&quot;created_date&quot;)
                .parameterValues(parameters)
                .sortKeys(sortKey)
                .build();

        itemReader.afterPropertiesSet();

        return itemReader;
    }

    private ItemWriter&amp;lt;? super OrderStatistics&amp;gt; orderStatisticsItemWriter(String date) throws Exception {
        YearMonth yearMonth = YearMonth.parse(date);

        String fileName = yearMonth.getYear() + &quot;년_&quot; + yearMonth.getMonthValue() + &quot;월_일별_주문_금액.csv&quot;;

        BeanWrapperFieldExtractor&amp;lt;OrderStatistics&amp;gt; fieldExtractor = new BeanWrapperFieldExtractor&amp;lt;&amp;gt;();
        fieldExtractor.setNames(new String[]{&quot;amount&quot;, &quot;date&quot;});

        DelimitedLineAggregator&amp;lt;OrderStatistics&amp;gt; lineAggregator = new DelimitedLineAggregator&amp;lt;&amp;gt;();
        lineAggregator.setDelimiter(&quot;,&quot;);
        lineAggregator.setFieldExtractor(fieldExtractor);

        FlatFileItemWriter&amp;lt;OrderStatistics&amp;gt; itemWriter = new FlatFileItemWriterBuilder&amp;lt;OrderStatistics&amp;gt;()
                .resource(new FileSystemResource(&quot;output/&quot; + fileName))
                .lineAggregator(lineAggregator)
                .name(JOB_NAME + &quot;_orderStatisticsItemWriter&quot;)
                .encoding(&quot;UTF-8&quot;)
                .headerCallback(writer -&amp;gt; writer.write(&quot;total_amount,date&quot;))
                .build();

        itemWriter.afterPropertiesSet();

        return itemWriter;
    }

    @Bean(JOB_NAME + &quot;_saveUserStep&quot;)
    public Step saveUserStep() {
        return this.stepBuilderFactory.get(JOB_NAME + &quot;_saveUserStep&quot;)
                .tasklet(new SaveUserTasklet(userRepository))
                .build();
    }

    @Bean(JOB_NAME + &quot;_userLevelUpStep&quot;)
    public Step userLevelUpStep() throws Exception {
        return this.stepBuilderFactory.get(JOB_NAME + &quot;_userLevelUpStep&quot;)
                .&amp;lt;User, User&amp;gt;chunk(CHUNK_SIZE)
                .reader(itemReader(null, null))
                .processor(itemProcessor())
                .writer(itemWriter())
                .build();
    }

    @Bean(JOB_NAME + &quot;_userLevelUpStep.manager&quot;)
    public Step userLevelUpManagerStep() throws Exception {
        return this.stepBuilderFactory.get(JOB_NAME + &quot;_userLevelUpStep.manager&quot;)
                .partitioner(JOB_NAME + &quot;_userLevelUpStep&quot;, new UserLevelUpPartitioner(userRepository))
                .step(userLevelUpStep())
                .partitionHandler(taskExecutorPartitionHandler())
                .build();
    }

    @Bean(JOB_NAME + &quot;taskExecutorPartitionHandler&quot;)
    PartitionHandler taskExecutorPartitionHandler() throws Exception {
        TaskExecutorPartitionHandler handler = new TaskExecutorPartitionHandler();
        handler.setStep(userLevelUpStep());
        handler.setTaskExecutor(this.taskExecutor);
        handler.setGridSize(8);

        return handler;
    }

    private ItemWriter&amp;lt;? super User&amp;gt; itemWriter() {
        return users -&amp;gt; users.forEach(x -&amp;gt; {
                x.levelUp();
                userRepository.save(x);
        });
    }

    private ItemProcessor&amp;lt;? super User, ? extends User&amp;gt; itemProcessor() {
        return user -&amp;gt; {
            if (user.availableLeveUp()) {
                return user;
            }

            return null;
        };
    }

    @Bean
    @StepScope //StepScope가 proxy로 설정되어있기 때문에 어떤 클래스를 리턴해줘야되는지 명확해야 한다.
    JpaPagingItemReader&amp;lt;? extends User&amp;gt; itemReader(@Value(&quot;#{stepExecutionContext[minId]}&quot;) Long minId,
                                          @Value(&quot;#{stepExecutionContext[maxId]}&quot;) Long maxId) throws Exception {

        Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;minId&quot;, minId);
        parameters.put(&quot;maxId&quot;, maxId);

        JpaPagingItemReader&amp;lt;User&amp;gt; itemReader = new JpaPagingItemReaderBuilder&amp;lt;User&amp;gt;()
                .queryString(&quot;select u from User u where u.id between :minId and :maxId&quot;)
                .parameterValues(parameters)
                .entityManagerFactory(entityManagerFactory)
                .pageSize(CHUNK_SIZE)
                .name(JOB_NAME + &quot;_userItemReader&quot;)
                .build();

        itemReader.afterPropertiesSet();

        return itemReader;
    }
}&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;작은 값과 큰 값을 구하기 위해 Partitioner를 활용한 UserLevelUpPartitioner를 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1628516049629&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class UserLevelUpPartitioner implements Partitioner {
    private final UserRepository userRepository;

    public UserLevelUpPartitioner(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public Map&amp;lt;String, ExecutionContext&amp;gt; partition(int gridSize) {
        long minId = userRepository.findMinId(); /* 가장 작은 Id값 (1번) */
        long maxId = userRepository.findMaxId(); /* 가장 큰 Id값 (40,000번) */

        /* 예시 : (40000-1) / 8 + 1 = 5000 */
        long targetSize = (maxId - minId) / gridSize + 1;

        /**
         * 값이 ExecutionContext에 저장
         * partion0 : 1, 5,000
         * partion1 : 5001, 10,000
         * ...
         * partion7 : 35001, 40,000
         */
        Map&amp;lt;String, ExecutionContext&amp;gt; result = new HashMap&amp;lt;&amp;gt;();

        long number = 0;
        long start = minId;
        long end = start + targetSize -1;

        while(start &amp;lt;= maxId) {
            ExecutionContext value = new ExecutionContext();

            result.put(&quot;partition&quot; + number, value);

            if (end &amp;gt;= maxId) {
                end = maxId;
            }

            value.putLong(&quot;minId&quot;, start);
            value.putLong(&quot;maxId&quot;, end);

            start += targetSize;
            end += targetSize;
            number++;
        }

        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;202&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E6QQO/btrbIcPD2JD/FkNmhKjlyK8E1zTrsAhvXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E6QQO/btrbIcPD2JD/FkNmhKjlyK8E1zTrsAhvXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E6QQO/btrbIcPD2JD/FkNmhKjlyK8E1zTrsAhvXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE6QQO%2FbtrbIcPD2JD%2FFkNmhKjlyK8E1zTrsAhvXK%2Fimg.png&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;202&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&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;&lt;span style=&quot;color: #000000;&quot;&gt;Async Step 보다는 빠르지만 &lt;b&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Multi-Thread Step &lt;/span&gt;&lt;/span&gt;&lt;/b&gt;보다 느렸다.&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;그렇다면 비동기방식과 콜라보로 한&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Async&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; + Partition Step는 어떨까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 Partition Step 에서 부분 수정한다.&lt;/span&gt;&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 alignLeft&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;797&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clYkaT/btrbENCUkav/dkYqGrDxx12PSd2yX6mN41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clYkaT/btrbENCUkav/dkYqGrDxx12PSd2yX6mN41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clYkaT/btrbENCUkav/dkYqGrDxx12PSd2yX6mN41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclYkaT%2FbtrbENCUkav%2FdkYqGrDxx12PSd2yX6mN41%2Fimg.png&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;797&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Async + Partition Step이&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Partition Step 보다 느린 성능이 나왔다...  &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이번엔 ParallelStep로 성능 측정을 알아보자.&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1137&quot; data-origin-height=&quot;277&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBe5zE/btrbtyGSOve/ickw7qtBm2Lx4sMGjTfzQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBe5zE/btrbtyGSOve/ickw7qtBm2Lx4sMGjTfzQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBe5zE/btrbtyGSOve/ickw7qtBm2Lx4sMGjTfzQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBe5zE%2FbtrbtyGSOve%2Fickw7qtBm2Lx4sMGjTfzQ0%2Fimg.png&quot; data-origin-width=&quot;1137&quot; data-origin-height=&quot;277&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;465&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vKJc7/btrbCtEGMfM/PFQyaodhVwgvbUbDk3PbSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vKJc7/btrbCtEGMfM/PFQyaodhVwgvbUbDk3PbSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vKJc7/btrbCtEGMfM/PFQyaodhVwgvbUbDk3PbSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvKJc7%2FbtrbCtEGMfM%2FPFQyaodhVwgvbUbDk3PbSk%2Fimg.png&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;465&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1628516827462&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@Slf4j
public class ParallelUserConfiguration {

    private final String JOB_NAME = &quot;parallelUserJob&quot;;
    public static final int CHUNK_SIZE = 1000;
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final UserRepository userRepository;
    private final EntityManagerFactory entityManagerFactory;
    private final DataSource dataSource;
    private final TaskExecutor taskExecutor;

    public ParallelUserConfiguration(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, UserRepository userRepository, EntityManagerFactory entityManagerFactory, DataSource dataSource, TaskExecutor taskExecutor) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
        this.userRepository = userRepository;
        this.entityManagerFactory = entityManagerFactory;
        this.dataSource = dataSource;
        this.taskExecutor = taskExecutor;
    }

    @Bean(JOB_NAME)
    public Job userJob() throws Exception {
        return this.jobBuilderFactory.get(JOB_NAME)
                .incrementer(new RunIdIncrementer())
                .listener(new LevelUpJobExecutionListener(userRepository))
                .start(this.saveUserFlow())
                .next(this.splitFlow(null))
                .build()
                .build();
    }

    @Bean(JOB_NAME + &quot;_saveUserFlow&quot;)
    public Flow saveUserFlow() {
        TaskletStep saveUserStep = this.stepBuilderFactory.get(JOB_NAME + &quot;_saveUserStep&quot;)
                .tasklet(new SaveUserTasklet(userRepository))
                .build();

        return new FlowBuilder&amp;lt;SimpleFlow&amp;gt;(JOB_NAME + &quot;_saveUserSlow&quot;)
                .start(saveUserStep)
                .build();
    }

    @Bean(JOB_NAME + &quot;_splitFlow&quot;)
    @JobScope
    public Flow splitFlow(@Value(&quot;#{jobParameters[date]}&quot;) String date) throws Exception {
        Flow userLevelUpFlow = new FlowBuilder&amp;lt;SimpleFlow&amp;gt;(JOB_NAME + &quot;_userLevelUpFlow&quot;)
                .start(userLevelUpStep())
                .build();

        // orderStatisticsFlow을 사용함으로써 아래의 Bean 객체에 등록 할 필요가 없다.
        // step 2개를 하나로 만들기 위해 splitFlow가 userLevelUpFlow와 orderStatisticsFlow를 병렬로 처리함.
        return new FlowBuilder&amp;lt;SimpleFlow&amp;gt;(JOB_NAME + &quot;_splitFlow&quot;)
                .split(this.taskExecutor)
                .add(userLevelUpFlow, orderStatisticsFlow(date))
                .build();
    }

    /**
     * @param date
     * @return
     *
     * userJob에 있는 step을 분리하여 Flow에 구현한다.
     */
    private Flow orderStatisticsFlow(String date) throws Exception {
        return new FlowBuilder&amp;lt;SimpleFlow&amp;gt;(JOB_NAME + &quot;_orderStatisticsFlow&quot;)
                .start(new JobParameterDecide(&quot;date&quot;)) // 가져온값이 아래의 CONTINUE인지 체크
                .on(JobParameterDecide.CONTINUE.getName()) // CONTINUE이면 아래의 to메서드 실행
                .to(this.orderStatisticsStep(date))
                .build();
    }

    public Step orderStatisticsStep(@Value(&quot;#{jobParameters[date]}&quot;) String date) throws Exception {
        return this.stepBuilderFactory.get(JOB_NAME + &quot;_orderStatisticsStep&quot;)
                .&amp;lt;OrderStatistics, OrderStatistics&amp;gt;chunk(CHUNK_SIZE)
                .reader(orderStatisticsItemReader(date))
                .writer(orderStatisticsItemWriter(date))
                .build();
    }

    private ItemReader&amp;lt;? extends OrderStatistics&amp;gt; orderStatisticsItemReader(String date) throws Exception {
        YearMonth yearMonth = YearMonth.parse(date);

        Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;startDate&quot;, yearMonth.atDay(1)); // 2021년 8월 1일
        parameters.put(&quot;endDate&quot;, yearMonth.atEndOfMonth()); // 8월의 마지막 날

        Map&amp;lt;String, Order&amp;gt; sortKey = new HashMap&amp;lt;&amp;gt;();
        sortKey.put(&quot;created_date&quot;, Order.ASCENDING);

        JdbcPagingItemReader&amp;lt;OrderStatistics&amp;gt; itemReader = new JdbcPagingItemReaderBuilder&amp;lt;OrderStatistics&amp;gt;()
                .dataSource(this.dataSource)
                .rowMapper((resultSet, i) -&amp;gt; OrderStatistics.builder()
                        .amount(resultSet.getString(1))
                        .date(LocalDate.parse(resultSet.getString(2), DateTimeFormatter.ISO_DATE))
                        .build())
                .pageSize(CHUNK_SIZE) // 페이징 설정
                .name(JOB_NAME + &quot;_orderStatisticsItemReader&quot;)
                .selectClause(&quot;sum(amount), created_date&quot;)
                .fromClause(&quot;orders&quot;)
                .whereClause(&quot;created_date &amp;gt;= :startDate and created_date &amp;lt;= :endDate&quot;)
                .groupClause(&quot;created_date&quot;)
                .parameterValues(parameters)
                .sortKeys(sortKey)
                .build();

        itemReader.afterPropertiesSet();

        return itemReader;
    }

    private ItemWriter&amp;lt;? super OrderStatistics&amp;gt; orderStatisticsItemWriter(String date) throws Exception {
        YearMonth yearMonth = YearMonth.parse(date);

        String fileName = yearMonth.getYear() + &quot;년_&quot; + yearMonth.getMonthValue() + &quot;월_일별_주문_금액.csv&quot;;

        BeanWrapperFieldExtractor&amp;lt;OrderStatistics&amp;gt; fieldExtractor = new BeanWrapperFieldExtractor&amp;lt;&amp;gt;();
        fieldExtractor.setNames(new String[]{&quot;amount&quot;, &quot;date&quot;});

        DelimitedLineAggregator&amp;lt;OrderStatistics&amp;gt; lineAggregator = new DelimitedLineAggregator&amp;lt;&amp;gt;();
        lineAggregator.setDelimiter(&quot;,&quot;);
        lineAggregator.setFieldExtractor(fieldExtractor);

        FlatFileItemWriter&amp;lt;OrderStatistics&amp;gt; itemWriter = new FlatFileItemWriterBuilder&amp;lt;OrderStatistics&amp;gt;()
                .resource(new FileSystemResource(&quot;output/&quot; + fileName))
                .lineAggregator(lineAggregator)
                .name(JOB_NAME + &quot;_orderStatisticsItemWriter&quot;)
                .encoding(&quot;UTF-8&quot;)
                .headerCallback(writer -&amp;gt; writer.write(&quot;total_amount,date&quot;))
                .build();

        itemWriter.afterPropertiesSet();

        return itemWriter;
    }

    @Bean(JOB_NAME + &quot;_userLevelUpStep&quot;)
    public Step userLevelUpStep() throws Exception {
        return this.stepBuilderFactory.get(JOB_NAME + &quot;_userLevelUpStep&quot;)
                .&amp;lt;User, User&amp;gt;chunk(CHUNK_SIZE)
                .reader(itemReader())
                .processor(itemProcessor())
                .writer(itemWriter())
                .build();
    }

    private ItemWriter&amp;lt;? super User&amp;gt; itemWriter() {
        return users -&amp;gt; users.forEach(x -&amp;gt; {
                x.levelUp();
                userRepository.save(x);
        });
    }

    private ItemProcessor&amp;lt;? super User, ? extends User&amp;gt; itemProcessor() {
        return user -&amp;gt; {
            if (user.availableLeveUp()) {
                return user;
            }

            return null;
        };
    }

    private ItemReader&amp;lt;? extends User&amp;gt; itemReader() throws Exception {
        JpaPagingItemReader&amp;lt;User&amp;gt; itemReader = new JpaPagingItemReaderBuilder&amp;lt;User&amp;gt;()
                .queryString(&quot;select u from User u&quot;)
                .entityManagerFactory(entityManagerFactory)
                .pageSize(CHUNK_SIZE)
                .name(JOB_NAME + &quot;_userItemReader&quot;)
                .build();

        itemReader.afterPropertiesSet();

        return itemReader;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Parallel Step은 오히려 현저하게 느려짐을 발견했다.&amp;nbsp;&lt;/span&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;&lt;span style=&quot;color: #000000;&quot;&gt;끝으로 &lt;span style=&quot;color: #000000;&quot;&gt;Partition +&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;span style=&quot;color: #000000;&quot;&gt;Parallel Step 테스트를 진행해 본다.&lt;/span&gt;&lt;/span&gt;&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Parallel Step&lt;span&gt; 에서 부분 수정 (비동기 개선, JpaPagingItemReader 리턴)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;905&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UxxLt/btrbAIayEVT/tQqzvMfk4kHILWgno4asWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UxxLt/btrbAIayEVT/tQqzvMfk4kHILWgno4asWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UxxLt/btrbAIayEVT/tQqzvMfk4kHILWgno4asWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUxxLt%2FbtrbAIayEVT%2FtQqzvMfk4kHILWgno4asWK%2Fimg.png&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;905&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같이 진행하지않고 ItemReader로만 선언하게되면 아래와 같은 에러를 발견할 수 있다.&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-origin-width=&quot;1746&quot; data-origin-height=&quot;663&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPDwCp/btrbAIBDXGJ/ucUk7qUOEmDC0iAkfgpciK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPDwCp/btrbAIBDXGJ/ucUk7qUOEmDC0iAkfgpciK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPDwCp/btrbAIBDXGJ/ucUk7qUOEmDC0iAkfgpciK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPDwCp%2FbtrbAIBDXGJ%2FucUk7qUOEmDC0iAkfgpciK%2Fimg.png&quot; data-origin-width=&quot;1746&quot; data-origin-height=&quot;663&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ItemReader -&amp;gt; JpaPagingItemReader로 리턴하면 문제 되지 않는다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Parallel Step보다 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;Partition +&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Parallel Step가 훨씬 속도가 개선되었다. 전체 순위에서 2위.&lt;/span&gt;&lt;/span&gt;&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 &lt;b&gt;멀티쓰레드 스텝&lt;/b&gt;을 쫓아갈 순 없었다.&lt;/span&gt;&lt;/span&gt;&lt;/span&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;* PC마다, 요구사항 환경마다 속도는 각각 다를 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&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;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여러가지 경험을 해봐야 다양한 케이스로 생각을 해볼 수 있어서 좋은 경험이었다.&lt;/span&gt;&lt;/span&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>SPRING/개발 TIP</category>
      <author>prodo-developer</author>
      <guid isPermaLink="true">https://prodo-developer.tistory.com/170</guid>
      <comments>https://prodo-developer.tistory.com/170#entry170comment</comments>
      <pubDate>Sat, 7 Aug 2021 18:46:48 +0900</pubDate>
    </item>
    <item>
      <title>[SPRING] 주문금액 집계 프로젝트 실습 part2</title>
      <link>https://prodo-developer.tistory.com/169</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://prodo-developer.tistory.com/168&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;part1에서 진행했던 내용을 기반&lt;/a&gt;으로 이어서 진행한다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;User의&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;totalAmount를&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; Orders &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Entity로&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;변경&lt;/span&gt;&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;하나의 User는 N개의 Orders를 포함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;주문&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; 총 &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;금액은&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; Orders &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Entity를&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;기준으로&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;합산&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;`-date=2021-08` &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;JobParameters&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;사용&lt;/span&gt;&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;주문 금액 집계는 orderStatisticsStep으로 생성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #595959;&quot;&gt;■ &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;`2021년_08월_주문_금액.csv` &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;파일은&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; 2021년 8월1일~8월31일 &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;주문&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;통계&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;내역&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #595959;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;`date` &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;파라미터가&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;없는&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;경우&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;orderStatisticsStep은&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;실행하지&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;않는다&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&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 alignLeft&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;427&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z0sa9/btraXSsqkVn/vg1avF3CK70ZTdeDIh7XzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z0sa9/btraXSsqkVn/vg1avF3CK70ZTdeDIh7XzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z0sa9/btraXSsqkVn/vg1avF3CK70ZTdeDIh7XzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz0sa9%2FbtraXSsqkVn%2Fvg1avF3CK70ZTdeDIh7XzK%2Fimg.png&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;427&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;user 한명이 orders의 여러개를 만들 수 있도록 일대다 구조로 만들 수 있게 엔티티를 수정한다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. Orders 추가&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1628082321485&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Getter
@NoArgsConstructor
public class Orders {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String itemName;

    private int amount;

    private LocalDate createdDate;

    @Builder
    public Orders(String itemName, int amount, LocalDate createdDate) {
        this.itemName = itemName;
        this.amount = amount;
        this.createdDate = createdDate;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. User 일대다 구조로 수정&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1628082360660&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Entity
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    @Enumerated(EnumType.STRING)
    private Level level = Level.NORMAL;

//    private int totalAmount;
    /**
     * 1:N 관계
     * user는 n개의 Orders를 가질 수 있다.
     * user가 저장되면서 Orders를 같이 저장할 수 있도록 영속성 전이(PERSIST)를 적용한다.
    **/
    @OneToMany(cascade = CascadeType.PERSIST)
    @JoinColumn(name = &quot;user_id&quot;)
    private List&amp;lt;Orders&amp;gt; orders;

    private LocalDate updatedDate;

    @Builder
    private User(String username, List&amp;lt;Orders&amp;gt; orders) {
        this.username = username;
        this.orders = orders;
    }

    public boolean availableLeveUp() {
        return Level.availableLevelUp(this.getLevel(), this.getTotalAmount());
    }

    private int getTotalAmount() {
        return this.orders.stream()
                .mapToInt(Orders::getAmount)
                .sum();
    }

    public Level levelUp() {
        Level nextLevel = Level.getNextLevel(this.getTotalAmount());
        this.level = nextLevel;
        this.updatedDate = LocalDate.now();

        return nextLevel;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. OrderStatistics (주문금액) 객체를 따로 분리한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1628083723020&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public class OrderStatistics {

    private String amount;

    private LocalDate date;

    @Builder
    public OrderStatistics(String amount, LocalDate date) {
        this.amount = amount;
        this.date = date;
    }
}&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4. UserConfiguration에 &lt;span style=&quot;color: #ffc66d;&quot;&gt;orderStatisticsStep&lt;span style=&quot;color: #000000;&quot;&gt;(주문금액집계)를 추가하여 월 단위 &lt;span style=&quot;color: #595959;&quot;&gt;주문&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;통계&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;내역을 조회하고 csv파일을 생성한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ffc66d;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #595959;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1628083489014&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@Slf4j
public class UserConfiguration {

    public static final int CHUNK_SIZE = 100;
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final UserRepository userRepository;
    private final EntityManagerFactory entityManagerFactory;
    private final DataSource dataSource;

    public UserConfiguration(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, UserRepository userRepository, EntityManagerFactory entityManagerFactory, DataSource dataSource) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
        this.userRepository = userRepository;
        this.entityManagerFactory = entityManagerFactory;
        this.dataSource = dataSource;
    }

    @Bean
    public Job userJob() throws Exception {
        return this.jobBuilderFactory.get(&quot;userJob&quot;)
                .incrementer(new RunIdIncrementer())
                .start(this.saveUserStep())
                .next(this.userLevelUpStep())
                .next(this.orderStatisticsStep(null))
                .listener(new LevelUpJobExecutionListener(userRepository))
                .build();
    }

    @Bean
    @JobScope
    public Step orderStatisticsStep(@Value(&quot;#{jobParameters[date]}&quot;) String date) throws Exception {
        return this.stepBuilderFactory.get(&quot;orderStatisticsStep&quot;)
                .&amp;lt;OrderStatistics, OrderStatistics&amp;gt;chunk(CHUNK_SIZE)
                .reader(orderStatisticsItemReader(date))
                .writer(orderStatisticsItemWriter(date))
                .build();
    }

    private ItemReader&amp;lt;? extends OrderStatistics&amp;gt; orderStatisticsItemReader(String date) throws Exception {
        YearMonth yearMonth = YearMonth.parse(date);

        Map&amp;lt;String, Object&amp;gt; parameters = new HashMap&amp;lt;&amp;gt;();
        parameters.put(&quot;startDate&quot;, yearMonth.atDay(1)); // 2021년 8월 1일
        parameters.put(&quot;endDate&quot;, yearMonth.atEndOfMonth()); // 8월의 마지막 날

        Map&amp;lt;String, Order&amp;gt; sortKey = new HashMap&amp;lt;&amp;gt;();
        sortKey.put(&quot;created_date&quot;, Order.ASCENDING);

        JdbcPagingItemReader&amp;lt;OrderStatistics&amp;gt; itemReader = new JdbcPagingItemReaderBuilder&amp;lt;OrderStatistics&amp;gt;()
                .dataSource(this.dataSource)
                .rowMapper((resultSet, i) -&amp;gt; OrderStatistics.builder()
                        .amount(resultSet.getString(1))
                        .date(LocalDate.parse(resultSet.getString(2), DateTimeFormatter.ISO_DATE))
                        .build())
                .pageSize(CHUNK_SIZE) // 페이징 설정
                .name(&quot;orderStatisticsItemReader&quot;)
                .selectClause(&quot;sum(amount), created_date&quot;)
                .fromClause(&quot;orders&quot;)
                .whereClause(&quot;created_date &amp;gt;= :startDate and created_date &amp;lt;= :endDate&quot;)
                .groupClause(&quot;created_date&quot;)
                .parameterValues(parameters)
                .sortKeys(sortKey)
                .build();

        itemReader.afterPropertiesSet();

        return itemReader;
    }

    private ItemWriter&amp;lt;? super OrderStatistics&amp;gt; orderStatisticsItemWriter(String date) throws Exception {
        YearMonth yearMonth = YearMonth.parse(date);

        String fileName = yearMonth.getYear() + &quot;년_&quot; + yearMonth.getMonthValue() + &quot;월_일별_주문_금액.csv&quot;;

        BeanWrapperFieldExtractor&amp;lt;OrderStatistics&amp;gt; fieldExtractor = new BeanWrapperFieldExtractor&amp;lt;&amp;gt;();
        fieldExtractor.setNames(new String[]{&quot;amount&quot;, &quot;date&quot;});

        DelimitedLineAggregator&amp;lt;OrderStatistics&amp;gt; lineAggregator = new DelimitedLineAggregator&amp;lt;&amp;gt;();
        lineAggregator.setDelimiter(&quot;,&quot;);
        lineAggregator.setFieldExtractor(fieldExtractor);

        FlatFileItemWriter&amp;lt;OrderStatistics&amp;gt; itemWriter = new FlatFileItemWriterBuilder&amp;lt;OrderStatistics&amp;gt;()
                .resource(new FileSystemResource(&quot;output/&quot; + fileName))
                .lineAggregator(lineAggregator)
                .name(&quot;orderStatisticsItemWriter&quot;)
                .encoding(&quot;UTF-8&quot;)
                .headerCallback(writer -&amp;gt; writer.write(&quot;total_amount,date&quot;))
                .build();

        itemWriter.afterPropertiesSet();

        return itemWriter;
    }

    @Bean
    public Step saveUserStep() {
        return this.stepBuilderFactory.get(&quot;saveUserStep&quot;)
                .tasklet(new SaveUserTasklet(userRepository))
                .build();
    }

    @Bean
    public Step userLevelUpStep() throws Exception {
        return this.stepBuilderFactory.get(&quot;userLevelUpStep&quot;)
                .&amp;lt;User, User&amp;gt;chunk(CHUNK_SIZE)
                .reader(itemReader())
                .processor(itemProcessor())
                .writer(itemWriter())
                .build();
    }

    private ItemWriter&amp;lt;? super User&amp;gt; itemWriter() {
        return users -&amp;gt; users.forEach(x -&amp;gt; {
                x.levelUp();
                userRepository.save(x);
        });
    }

    private ItemProcessor&amp;lt;? super User, ? extends User&amp;gt; itemProcessor() {
        return user -&amp;gt; {
            if (user.availableLeveUp()) {
                return user;
            }

            return null;
        };
    }

    private ItemReader&amp;lt;? extends User&amp;gt; itemReader() throws Exception {
        JpaPagingItemReader&amp;lt;User&amp;gt; itemReader = new JpaPagingItemReaderBuilder&amp;lt;User&amp;gt;()
                .queryString(&quot;select u from User u&quot;)
                .entityManagerFactory(entityManagerFactory)
                .pageSize(CHUNK_SIZE)
                .name(&quot;userItemReader&quot;)
                .build();

        itemReader.afterPropertiesSet();

        return itemReader;
    }
}&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;5. JobExecutionDecider에서 제공하는 주문 금액 집계 step 실행 여부 벨리데이션을 구현한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1628084133932&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JobParameterDecide implements JobExecutionDecider {

    public static final FlowExecutionStatus CONTINUE = new FlowExecutionStatus(&quot;CONTINUE&quot;);

    private final String key;

    public JobParameterDecide(String key) {
        this.key = key;
    }

    @Override
    public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
        String value = jobExecution.getJobParameters().getString(key);

        // key가 없으면
        if(StringUtils.isEmpty(value)) {
            return FlowExecutionStatus.COMPLETED;
        }

        return CONTINUE;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;* JobExecutionDecider의 인터페이스는 flow결정과_배치실행여부를 결정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;777&quot; data-filename=&quot;5_flow결정과_배치실행여부.PNG&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qdS6V/btra09AKRsr/Tk1ZEmDNiuz0vxeMXyQFAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qdS6V/btra09AKRsr/Tk1ZEmDNiuz0vxeMXyQFAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qdS6V/btra09AKRsr/Tk1ZEmDNiuz0vxeMXyQFAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqdS6V%2Fbtra09AKRsr%2FTk1ZEmDNiuz0vxeMXyQFAK%2Fimg.png&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;777&quot; data-filename=&quot;5_flow결정과_배치실행여부.PNG&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;6. 5번에서 만든 JobExecutionDecide를 기반으로 step 기능을 추가한다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;* UserConfiguration.java 의 &lt;span style=&quot;color: #ffc66d;&quot;&gt;userJob&lt;/span&gt;() 영역 step 추가&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1628084383708&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public Job userJob() throws Exception {
	return this.jobBuilderFactory.get(&quot;userJob&quot;)
		.incrementer(new RunIdIncrementer())
        .start(this.saveUserStep())
        .next(this.userLevelUpStep())
        .listener(new LevelUpJobExecutionListener(userRepository))
        .next(new JobParameterDecide(&quot;date&quot;)) // 가져온값이 아래의 CONTINUE인지 체크
        .on(JobParameterDecide.CONTINUE.getName()) // CONTINUE이면 아래의 to메서드 실행
        .to(this.orderStatisticsStep(null))
        .build()
        .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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;* 결과 값&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;973&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pkuI2/btrbmx0mb1W/7cvkicQDNF6UuzE7geZ3m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pkuI2/btrbmx0mb1W/7cvkicQDNF6UuzE7geZ3m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pkuI2/btrbmx0mb1W/7cvkicQDNF6UuzE7geZ3m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpkuI2%2Fbtrbmx0mb1W%2F7cvkicQDNF6UuzE7geZ3m1%2Fimg.png&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;973&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&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;느낀점 :&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;&amp;nbsp;user가 저장되면서 Orders를 같이 저장할 수 있도록 영속성 전이(PERSIST)를 적용 함으로 써 JPA의 이해를 한번 더 상기 시켰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(잊을만 하면 기존에 기록했던 &lt;a href=&quot;https://prodo-developer.tistory.com/151&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;영속성 전이&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;2. &lt;span&gt;JobExecutionDecider를 통해 배치실행여부를 좀 더 쉽게 확인이 가능한점을 배움.&lt;/span&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;3.&amp;nbsp;FlatFileItemWriter로 데이터 CSV 파일 쓰기를 통해 보다 편하게 생성할 수 있었다.&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;</description>
      <category>SPRING/개발 TIP</category>
      <author>prodo-developer</author>
      <guid isPermaLink="true">https://prodo-developer.tistory.com/169</guid>
      <comments>https://prodo-developer.tistory.com/169#entry169comment</comments>
      <pubDate>Wed, 4 Aug 2021 22:40:24 +0900</pubDate>
    </item>
    <item>
      <title>[SPRING] 주문금액 집계 프로젝트 실습 part1</title>
      <link>https://prodo-developer.tistory.com/168</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;JPA를 통한 스프링 배치 실습 프로젝트를 진행하면서 아래의 요구사항이 있다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;User &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;등급을&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; 4개로 &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;구분&lt;/span&gt;&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;일반(NORMAL), 실버(SILVER), 골드(GOLD), VIP&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;User &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;등급&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;상향&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;조건은&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; 총 &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;주문&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;금액&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;기준으로&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;등급&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;상향&lt;/span&gt;&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;200,000원 이상인 경우 실버로 상향&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;300,000원 이상인 경우 골드로 상향&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;500,000원 이상인 경우 VIP로 상향&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;등급 하향은 없음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;총 2개의 &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;Step으로&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;회원&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;등급&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; Job &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;생성&lt;/span&gt;&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;saveUserStep : User 데이터 저장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;userLevelUpStep : User 등급 상향&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #595959;&quot;&gt;● &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;JobExecutionListener.afterJob&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;메소드에서&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &amp;ldquo;총 &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;데이터&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;처리&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; {}건, &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;처리&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;시간&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; : {}&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;millis&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;&amp;rdquo; 와 &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;같은&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;로그&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;출력&lt;/span&gt;&lt;/span&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;span style=&quot;color: #595959; font-family: 'Nanum Gothic';&quot;&gt;1. 요구사항에 필요한 User와 Level별 엔티티를 아래와 같이 만든다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;User.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1628080147000&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Entity
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    @Enumerated(EnumType.STRING)
    private Level level = Level.NORMAL;

    private int totalAmount;

    private LocalDateTime updateDate;

    @Builder
    private User(String username, int totalAmount) {
        this.username = username;
        this.totalAmount = totalAmount;
    }

    public boolean availabelLevelUp() {
        return Level.availabelLevelUp(this.getLevel(), this.getTotalAmount());
    }

    public Level levelUp() {
        Level nextLevel = Level.getNextLevel(this.getTotalAmount());
        this.level = nextLevel;
        this.updateDate = LocalDateTime.now();

        return nextLevel;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #595959; font-family: 'Nanum Gothic';&quot;&gt;Level.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1628080168262&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum Level {
    VIP(500_000, null),
    GOLD(500_000, VIP),
    SILVER(300_000, GOLD),
    NORMAL(200_000, SILVER);

    private final int nextAmount;
    private final Level nextLevel;

    Level(int nextAmount, Level nextLevel) {
        this.nextAmount = nextAmount;
        this.nextLevel = nextLevel;
    }

    public static boolean availabelLevelUp(Level level, int totalAmount) {
        if(Objects.isNull(level)) {
            return false;
        }

        if(Objects.isNull(level.nextLevel)) {
            return false;
        }

        return totalAmount &amp;gt;= level.nextAmount;
    }

    static Level getNextLevel(int totalAmount) {
        if(totalAmount &amp;gt;= Level.VIP.nextAmount) {
            return VIP;
        }

        // GOLD.nextLevel == VIP
        if(totalAmount &amp;gt;= Level.GOLD.nextAmount) {
            return GOLD.nextLevel;
        }

        // SILVER.nextLevel == GOLD
        if(totalAmount &amp;gt;= Level.SILVER.nextAmount) {
            return SILVER.nextLevel;
        }

        // NORMAL.nextLevel == SILVER
        if(totalAmount &amp;gt;= Level.NORMAL.nextAmount) {
            return NORMAL.nextLevel;
        }

        return NORMAL;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #595959; font-family: 'Nanum Gothic';&quot;&gt;2. 100건당 등급 상향조건에 맞는 Tasklet 생성&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1628080211000&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SaveUserTasklet implements Tasklet {

    private final int ZERO = 0;
    private final int SIZE = 100;
    private final UserRepository userRepository;

    public SaveUserTasklet(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        List&amp;lt;User&amp;gt; users = createUsers();

        Collections.shuffle(users);

        userRepository.saveAll(users);

        return RepeatStatus.FINISHED;
    }

    private List&amp;lt;User&amp;gt; createUsers() {
        List&amp;lt;User&amp;gt; users = new ArrayList&amp;lt;&amp;gt;();

        for (int i = ZERO; i &amp;lt; SIZE; i++) {
            users.add(User.builder()
                    .orders(Collections.singletonList(Orders.builder()
                            .amount(1_000)
                            .createdDate(LocalDate.of(2021,8,1))
                            .itemName(&quot;item&quot; +i)
                            .build()))
                    .username(&quot;test username&quot; + i)
                    .build());
        }

        // SILVER 등급
       for (int i = ZERO; i &amp;lt; SIZE; i++) {
            users.add(User.builder()
                    .orders(Collections.singletonList(Orders.builder()
                            .amount(200_000)
                            .createdDate(LocalDate.of(2021,9,2))
                            .itemName(&quot;item&quot; +i)
                            .build()))
                    .username(&quot;test username&quot; + i)
                    .build());
        }

        // GOLD 등급
        for (int i = ZERO; i &amp;lt; SIZE; i++) {
            users.add(User.builder()
                    .orders(Collections.singletonList(Orders.builder()
                            .amount(300_000)
                            .createdDate(LocalDate.of(2021,8,3))
                            .itemName(&quot;item&quot; +i)
                            .build()))
                    .username(&quot;test username&quot; + i)
                    .build());
        }

        // VIP 등급
        for (int i = ZERO; i &amp;lt; SIZE; i++) {
            users.add(User.builder()
                    .orders(Collections.singletonList(Orders.builder()
                            .amount(500_000)
                            .createdDate(LocalDate.of(2021,8,4))
                            .itemName(&quot;item&quot; +i)
                            .build()))
                    .username(&quot;test username&quot; + i)
                    .build());
        }

        return users;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. 배치 작업에 필요한 Configuration를 생성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1628080290382&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@Slf4j
public class UserConfiguration {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final UserRepository userRepository;
    private final EntityManagerFactory entityManagerFactory;

    public UserConfiguration(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, UserRepository userRepository, EntityManagerFactory entityManagerFactory) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
        this.userRepository = userRepository;
        this.entityManagerFactory = entityManagerFactory;
    }

    @Bean
    public Job userJob() throws Exception {
        return this.jobBuilderFactory.get(&quot;userJob&quot;)
                .incrementer(new RunIdIncrementer())
                .start(this.saveUserStep())
                .next(this.userLevelUpStep())
                .listener(new LevelUpJobExecutionListener(userRepository))
                .build();
    }

    @Bean
    public Step saveUserStep() {
        return this.stepBuilderFactory.get(&quot;saveUserStep&quot;)
                .tasklet(new SaveUserTasklet(userRepository))
                .build();
    }

    @Bean
    public Step userLevelUpStep() throws Exception {
        return this.stepBuilderFactory.get(&quot;userLevelUpStep&quot;)
                .&amp;lt;User, User&amp;gt;chunk(100)
                .reader(itemReader())
                .processor(itemProcessor())
                .writer(itemWriter())
                .build();
    }

    private ItemWriter&amp;lt;? super User&amp;gt; itemWriter() {
        return users -&amp;gt; users.forEach(x -&amp;gt; {
                x.levelUp();
                userRepository.save(x);
        });
    }

    private ItemProcessor&amp;lt;? super User, ? extends User&amp;gt; itemProcessor() {
        return user -&amp;gt; {
            if (user.availableLeveUp()) {
                return user;
            }

            return null;
        };
    }

    private ItemReader&amp;lt;? extends User&amp;gt; itemReader() throws Exception {
        JpaPagingItemReader&amp;lt;User&amp;gt; itemReader = new JpaPagingItemReaderBuilder&amp;lt;User&amp;gt;()
                .queryString(&quot;select u from User u&quot;)
                .entityManagerFactory(entityManagerFactory)
                .pageSize(100)
                .name(&quot;userItemReader&quot;)
                .build();

        itemReader.afterPropertiesSet();

        return itemReader;
    }
}&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #595959;&quot;&gt;1. Juni&lt;/span&gt;&lt;span style=&quot;color: #595959;&quot;&gt;t4 실습 위주의 단위 테스트 였으나 요즘 대세인 Junit5기반으로 셋팅을 해보았다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1825&quot; data-origin-height=&quot;723&quot; data-filename=&quot;@SpringBatchTest를 통한 해결.PNG&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nk7gR/btrbhZDhN9B/8zlUIkjGFGrbIB6mJKBDr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nk7gR/btrbhZDhN9B/8zlUIkjGFGrbIB6mJKBDr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nk7gR/btrbhZDhN9B/8zlUIkjGFGrbIB6mJKBDr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnk7gR%2FbtrbhZDhN9B%2F8zlUIkjGFGrbIB6mJKBDr0%2Fimg.png&quot; data-origin-width=&quot;1825&quot; data-origin-height=&quot;723&quot; data-filename=&quot;@SpringBatchTest를 통한 해결.PNG&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;* 메인메서드 셋팅은 아래와 같이 진행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;607&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zY6y0/btrbmxMNPss/I997Vie2PqMLahbP4pTWdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zY6y0/btrbmxMNPss/I997Vie2PqMLahbP4pTWdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zY6y0/btrbmxMNPss/I997Vie2PqMLahbP4pTWdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzY6y0%2FbtrbmxMNPss%2FI997Vie2PqMLahbP4pTWdk%2Fimg.png&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;607&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&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;pre id=&quot;code_1628081619101&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
public class LevelUpJobExecutionListener implements JobExecutionListener {

    private final UserRepository userRepository;

    public LevelUpJobExecutionListener(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public void beforeJob(JobExecution jobExecution) {

    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        Collection&amp;lt;User&amp;gt; users = userRepository.findAllByUpdatedDate(LocalDate.now());

        long time = jobExecution.getEndTime().getTime() - jobExecution.getStartTime().getTime();

        log.info(&quot;회원등급 업데이트 배치 프로그램&quot;);
        log.info(&quot;--------------------------&quot;);
        log.info(&quot;총 데이터 처리 {}건, 처리 시간 {}mills&quot;, users.size(), time);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;* 결과&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;161&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BN6oC/btrbgnEpkBr/5kAPFLOm0jn9uzN0hHQITK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BN6oC/btrbgnEpkBr/5kAPFLOm0jn9uzN0hHQITK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BN6oC/btrbgnEpkBr/5kAPFLOm0jn9uzN0hHQITK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBN6oC%2FbtrbgnEpkBr%2F5kAPFLOm0jn9uzN0hHQITK%2Fimg.png&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;161&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&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;&lt;b&gt;느낀점 : step별 배치를 돌리면서 처리되는시간을 통해 좀 더 최적화 할수 있는 방법에 대해 고민하게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>SPRING/개발 TIP</category>
      <author>prodo-developer</author>
      <guid isPermaLink="true">https://prodo-developer.tistory.com/168</guid>
      <comments>https://prodo-developer.tistory.com/168#entry168comment</comments>
      <pubDate>Wed, 4 Aug 2021 21:56:08 +0900</pubDate>
    </item>
  </channel>
</rss>