<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Jadie</title>
    <link>https://yongdev.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 12 Aug 2026 04:13:03 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Jadie Blog</managingEditor>
    <image>
      <title>Jadie</title>
      <url>https://tistory1.daumcdn.net/tistory/1970232/attach/b969c2280b76450e8541ebf429bbe6ee</url>
      <link>https://yongdev.tistory.com</link>
    </image>
    <item>
      <title>SpringBoot Master/Slave DB 세팅 2</title>
      <link>https://yongdev.tistory.com/169</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난번 포스팅에서는 Master/Slave DB를 사용하기 위해 어떻게 세팅을 하는지에 대해 다뤘다. 이러한 세팅을 적용하면서&amp;nbsp; 궁금증이 생겼다. 세팅을 하기 위해 자료를 찾은 것과 그동안 공부했던 것들을 바탕으로 대충은 어떻게 동작하는지는 알게되었으나 더 자세히 알고 싶어졌다. 그래서 이번 글에서는 이 세팅이 어떻게 동작하는지 알아보기위해 코드를 훑어보는 시간을 가져보려고 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;개인 공부 용으로 남겨 놓는거라 복잡합니다. 핵심 부분은 LazyConnectionDataSourceProxy에 있으니 그 부분만 보시는 것을 추천합니다.&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;실행할 로직은 간단한 조회 API 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1708867529429&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
public class AppApi {
    private final AppService appService;

    public StoreAppApi(appService appService) {
        this.appService = appService;
    }

    @GetMapping(&quot;/apps&quot;)
    public ApiResponse&amp;lt;?&amp;gt; getApps() {
        return ApiResponse.of(&quot;00&quot;, &quot;성공&quot;, appService.getAllApps());
    }

}

@Service
public class AppService {
    private final AppRepository appRepository;

    public AppService(AppRepository appRepository) {
        this.appRepository = appRepository;
    }

    @Transactional(readOnly = true)
    public AppListResponse getAllApps() {
        List&amp;lt;App&amp;gt; apps = appRepository.findAll();
        return AppListResponse.of(apps.stream()
                .map(App::getAppName)
                .collect(Collectors.joining(&quot;,&quot;))) ;
    }
}

public interface AppRepository extends Repository&amp;lt;App, Long&amp;gt; {
    List&amp;lt;App&amp;gt; findAll();
}&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;이전 글에서의 세팅을 적용한 상태다.. readOnly=ture 를 적용했으니 최종적으로 &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;lazyConnectionDataSourceProxy 를 통해&lt;/span&gt; slave DB 를 사용해야 한다.&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;처음에 어플리케이션을 실행하면 스프링 빈을 생성한다. RoutingDataSource 를 생성하고 L&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;azyConnectionDataSourceProxy&lt;span&gt; 를 생성한다. @DependsOn 어노테이션 때문에 RoutingDataSource 를 생성하고 LazyConnectionDataSourceProxy 를 생성하는 것이다. 그다음 EntityManagerFactory 와 TransactionManager 를 생성한다.&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span&gt;이제 API 를 호출해보겠다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span&gt;트랜잭션 생성&lt;/span&gt;&lt;/span&gt;&lt;/h2&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: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span&gt; 컨트롤러를 통해 &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;@Transactional&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 이 적용된 서비스 메서드를 호출한다. 그러면 CglibAopProxy 를 통해 트랜잭션 생성을 시작한다.&amp;nbsp; TransactionInterceptor -&amp;gt; TransactionAspectSupport#invokeWithinTransaction 메서드를 를 호출하게 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KJIYf/btsFsa9rs2P/VSm8KW8YRfVLsLQucvfeEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KJIYf/btsFsa9rs2P/VSm8KW8YRfVLsLQucvfeEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KJIYf/btsFsa9rs2P/VSm8KW8YRfVLsLQucvfeEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKJIYf%2FbtsFsa9rs2P%2FVSm8KW8YRfVLsLQucvfeEK%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;932&quot; height=&quot;336&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 메서드 에서 트랜잭션의 속성을 얻고 트랜잭션 매니저를 결정한다. 트랜잭션 속성은 @Transactional 에 정의된 속성이다. 이 트랜잭션에서는 PROPAGATION_REQUIRED, ISOLATION_DEFAULT, readOnly 속성이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;895&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9fizf/btsFqZUt0Nj/Hap1Kcxv5Dg9NuC2CLav3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9fizf/btsFqZUt0Nj/Hap1Kcxv5Dg9NuC2CLav3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9fizf/btsFqZUt0Nj/Hap1Kcxv5Dg9NuC2CLav3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9fizf%2FbtsFqZUt0Nj%2FHap1Kcxv5Dg9NuC2CLav3k%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;895&quot; height=&quot;504&quot; data-origin-width=&quot;895&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;determineTransactionManager 에서는 기존의 트랜잭션 매니저를 얻거나 캐시에 있는 트랜잭션 매니저를 얻어온다. 하지만 기존에 사용하는 트랜잭션이 없기 때문에 beanFactory 를 통해 만들어진 트랜잭션 매니저 빈을 가져온다.&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;903&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cag06k/btsFqY86paU/i2wzUNUTkm6K4DWT75wWc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cag06k/btsFqY86paU/i2wzUNUTkm6K4DWT75wWc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cag06k/btsFqY86paU/i2wzUNUTkm6K4DWT75wWc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcag06k%2FbtsFqY86paU%2Fi2wzUNUTkm6K4DWT75wWc1%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;903&quot; height=&quot;407&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이제 TransactionalAspectSupport 로 돌아오면 트랜잭션을 만드는 로직을 볼 수 있다.createTransactionIfNecessary 메서드로 들어가면 트랜잭션 매니저를 통해 트랜잭션을 얻는 로직이 있다. tm.getTransaction 을 호출하면 PlatformTransactionManager 구현체인 AbstractPlatformTransactionManager#getTransaction 을 호출하게 된다.&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;AbstractPlatformTransactionManager#getTransaction&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dG3ro9/btsFnVk8DWZ/AU9XqJnWobkh3NPJw9I1YK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dG3ro9/btsFnVk8DWZ/AU9XqJnWobkh3NPJw9I1YK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dG3ro9/btsFnVk8DWZ/AU9XqJnWobkh3NPJw9I1YK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdG3ro9%2FbtsFnVk8DWZ%2FAU9XqJnWobkh3NPJw9I1YK%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;847&quot; height=&quot;330&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;330&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;여기서 doGetTransaction() 은 추상메서드이다. 이 메서드를 호출하면 트랜잭션 매니저 빈을 정의할 때 사용한 JpaTransactionManager의 메서드를 호출하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjipmB/btsFsZT85TG/JdLvtK1X0TRkmoCCzb2X9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjipmB/btsFsZT85TG/JdLvtK1X0TRkmoCCzb2X9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjipmB/btsFsZT85TG/JdLvtK1X0TRkmoCCzb2X9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjipmB%2FbtsFsZT85TG%2FJdLvtK1X0TRkmoCCzb2X9k%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;777&quot; height=&quot;512&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;트랜잭션 동기화 매니저를 통해 EntityManagerHolder, ConnectionHolder 를 얻는다. 이 메서드에서는 둘다 null을 얻게 된다. 첫번째 트랜잭션이기 때문에 등록된게 없어 그런것 같다. 참고로 getDataSource() 메서드를 통해 등록된 DataSource 가 있는지 확인하는데, 여기서는 LazyConnectionDataSource 가 등록되어 있다. 이제 AbstractPlatformTransactionManager 로 돌아가 다음 로직을 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bk7ebt/btsFq1dGJmH/BcnolwtVZbDtacb7sCxL5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bk7ebt/btsFq1dGJmH/BcnolwtVZbDtacb7sCxL5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bk7ebt/btsFq1dGJmH/BcnolwtVZbDtacb7sCxL5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbk7ebt%2FbtsFq1dGJmH%2FBcnolwtVZbDtacb7sCxL5k%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;840&quot; height=&quot;666&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isExistingTransaction 은 기존에 트랜잭션이 있는지 확인한다.&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;791&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bucV51/btsFvXn3hsF/fshGAajWxdkr64XNQHsCwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bucV51/btsFvXn3hsF/fshGAajWxdkr64XNQHsCwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bucV51/btsFvXn3hsF/fshGAajWxdkr64XNQHsCwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbucV51%2FbtsFvXn3hsF%2FfshGAajWxdkr64XNQHsCwK%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;791&quot; height=&quot;93&quot; data-origin-width=&quot;791&quot; data-origin-height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 로직을 확인해보면 JpaTransactionManager#hasTransaction 에 entityManagerHolder 를 확인하는데 현재 사용중인 entityManageHolder 가 없기 때문에 isExistingTransaction 은 false 가 나오게 된다. 첫번째 트랜잭션 생성 로직을 수행한 이후의 트랜잭션을 마주하게 되면 true 로 나와 해당 로직을 수행하게 되는 것 같다.&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;그 다음에는 PROPAGATION_MANDATORY 면 예외를 일으키는 로직이 있다. PROPAGATION_MANDATORY 는 이전 트랜잭션이 존재하면 그 이전 트랜잭션을 사용하는 옵션이다. 그리고 이전 트랜잭션은 반드시 존재해야만 한다. 만약 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;PROPAGATION_MANDATORY&lt;span&gt; 설정이고 이전 트랜잭션이 없는 첫번째 트랜잭션이면 예외가 발생한다. 그리고 이제 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;AbstractPlatformTransactionManager&lt;span&gt; 의 startTransaction을 실행해보자.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&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;h3 data-ke-size=&quot;size23&quot;&gt;AbstractPlatformTransactionManager#startTransaction&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;987&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3jVTN/btsFodza8JF/HJ8U8LFfKtH34UnuPxEQk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3jVTN/btsFodza8JF/HJ8U8LFfKtH34UnuPxEQk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3jVTN/btsFodza8JF/HJ8U8LFfKtH34UnuPxEQk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3jVTN%2FbtsFodza8JF%2FHJ8U8LFfKtH34UnuPxEQk0%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;987&quot; height=&quot;234&quot; data-origin-width=&quot;987&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;기존에 활성화된 트랜잭션 매니저가 없으니 newSynchronization 은 true 이다. doBegin은 추상메서드다. 이 메서드를 호출하면 JpaTransactionManager#doBegin 이 실행된다.&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;1011&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kz66i/btsFoHNyt2M/uVVzZmu2LtRSGcweBgNXqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kz66i/btsFoHNyt2M/uVVzZmu2LtRSGcweBgNXqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kz66i/btsFoHNyt2M/uVVzZmu2LtRSGcweBgNXqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkz66i%2FbtsFoHNyt2M%2FuVVzZmu2LtRSGcweBgNXqk%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;1011&quot; height=&quot;648&quot; data-origin-width=&quot;1011&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;createEntityManagerForTransaction 메서드를 호출하면 엔티티 매니저를 만든다.&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;808&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4wwKB/btsFoEXKyPM/65GjxSKdfLP4k7b1eOYjYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4wwKB/btsFoEXKyPM/65GjxSKdfLP4k7b1eOYjYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4wwKB/btsFoEXKyPM/65GjxSKdfLP4k7b1eOYjYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4wwKB%2FbtsFoEXKyPM%2F65GjxSKdfLP4k7b1eOYjYK%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;808&quot; height=&quot;414&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;createNativeEntityManager 메서드를 호출하면 위와 같이 SessionImpl 이 생성된다. 생성된 newEm 엔티티 매니저 객체를 EntityManagerHolder 로 생성해 트랜잭션에 set 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh54hH/btsFsb1AF9Z/Tx9r7pCRzxqeK0FnpcorN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh54hH/btsFsb1AF9Z/Tx9r7pCRzxqeK0FnpcorN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh54hH/btsFsb1AF9Z/Tx9r7pCRzxqeK0FnpcorN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh54hH%2FbtsFsb1AF9Z%2FTx9r7pCRzxqeK0FnpcorN1%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;844&quot; height=&quot;193&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 생성된 엔티티 매니저를 사용해 getJpaDialect().beginTransaction 을 호출하자. Hibernate를 사용했으니 HibernateJpaDialect #beginTransaction 을 호출한다.&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;932&quot; data-origin-height=&quot;641&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qROAW/btsFsdyks8y/LWHWZvrSjCfN4dZNA9fZJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qROAW/btsFsdyks8y/LWHWZvrSjCfN4dZNA9fZJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qROAW/btsFsdyks8y/LWHWZvrSjCfN4dZNA9fZJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqROAW%2FbtsFsdyks8y%2FLWHWZvrSjCfN4dZNA9fZJK%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;932&quot; height=&quot;641&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;641&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;DataSourceUtils.prepareConnectionForTransaction 메서드에서 @Transactional 에 정의된 속성들을 이용해 커넥션에 세팅을 한다.&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;831&quot; data-origin-height=&quot;317&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vifld/btsFoCZW6nb/CaDGCvgemkBUKumvokZ9L0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vifld/btsFoCZW6nb/CaDGCvgemkBUKumvokZ9L0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vifld/btsFoCZW6nb/CaDGCvgemkBUKumvokZ9L0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvifld%2FbtsFoCZW6nb%2FCaDGCvgemkBUKumvokZ9L0%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;831&quot; height=&quot;317&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;317&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그리고 begin() 메서드를 호출해 코드를 추적하다 보면 AbstractLogicalConnectionImplementor#begin() 메서드를 호출해&amp;nbsp; 트랜잭션의 상태를 ACTIVE 로 변경한다. 이제 JpaTransactionManager 로 돌아오면 아래와 같은 로직을 만나게 된다.&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;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;485&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccgfaS/btsFpRP3nY4/UG5lkjWr3HcPsDEjlSqJW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccgfaS/btsFpRP3nY4/UG5lkjWr3HcPsDEjlSqJW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccgfaS/btsFpRP3nY4/UG5lkjWr3HcPsDEjlSqJW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccgfaS%2FbtsFpRP3nY4%2FUG5lkjWr3HcPsDEjlSqJW0%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;892&quot; height=&quot;485&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;485&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;jpaDialect 의 JdbcConnection 을 호출하면 HibernateJpaDialect#getJdbcConnection 을 호출하게 된다. Connection을 가져와 ConnectionHolder 를 만들고 트랜잭션 객체에 ConnectionHolder 를 설정한다. TransactionSynchronizationManager 의 bindResource 메서드는 key, value 를 파라미터로 받아 리소스로 등록하는 메서드다.&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;878&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYqxog/btsFq1dG4a0/PtdLlw2SDUwUctRk8SQUD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYqxog/btsFq1dG4a0/PtdLlw2SDUwUctRk8SQUD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYqxog/btsFq1dG4a0/PtdLlw2SDUwUctRk8SQUD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYqxog%2FbtsFq1dG4a0%2FPtdLlw2SDUwUctRk8SQUD0%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;878&quot; height=&quot;146&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위에서는 DataSource를 key 로 ConnectionHolder value 로 저장했고, 그다음에는 EntityManagerFactory 를 key, EntityManagerHolder 를 value 로 저장한다. 그리고 트랜잭션의 EntityManagerHolder 를 가져와 싱크를 true 로 설정하는 로직이 있다. 여기까지가 AbstractPlatformTransactionManager#startTransaction 메서드의 끝이다. 이후엔 TransactionAspectSupport -&amp;gt; TransactionInterceptor -&amp;gt; CglibAopProxy 로 돌아와 AOP 로 감싸고 있는 비즈니스 로직을 실행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AppRepository.findAll()&lt;/h2&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;AppRepository는 Repository 를 구현한 인터페이스다. 모든 APP 을 조회하는 로직이고 해당 로직을 실행하면&amp;nbsp; 위에서 트랜잭션을 실행할 때와 같이 CglibAopProxy -&amp;gt; TransactionInterceptor -&amp;gt; TransactionAspectSupport#invokeWithinTransaction 이 호출된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1005&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MkIr3/btsFuOLBLxx/MdKdm2AvjJWwqRqQdCjKfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MkIr3/btsFuOLBLxx/MdKdm2AvjJWwqRqQdCjKfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MkIr3/btsFuOLBLxx/MdKdm2AvjJWwqRqQdCjKfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMkIr3%2FbtsFuOLBLxx%2FMdKdm2AvjJWwqRqQdCjKfk%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;1005&quot; height=&quot;542&quot; data-origin-width=&quot;1005&quot; data-origin-height=&quot;542&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위에서 호출 했을 땐 transactionManagerBean 이 값이 없어서 아래의 로직을 실행했는데, 여기서는 트랜잭션이 실행된 후니까 determineQualifiedTransactionalManager() 를 호출하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cig1Xh/btsFv0E48Zc/VoT1oTIoNYafKkHb5m6YC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cig1Xh/btsFv0E48Zc/VoT1oTIoNYafKkHb5m6YC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cig1Xh/btsFv0E48Zc/VoT1oTIoNYafKkHb5m6YC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcig1Xh%2FbtsFv0E48Zc%2FVoT1oTIoNYafKkHb5m6YC1%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;978&quot; height=&quot;213&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;213&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이전에 생성한 트랜잭션 매니저가 캐쉬에는 등록되지 않았으니 txManager 는 null 을 가진다. 여기서 transactionManagerCache 에 트랜잭션 매니저를 저장한다.&amp;nbsp; 이후에 createTransactionIfNecessary 를 호출해 트랜잭션 매니저의 getTransaction 을 호출한다. 그러면 구현체인 AbstractPlatformTransactionManager 의 getTransaction 을 호출하게 된다. getTransaction() 에서 doGetTransaction 메서드를 호출하면 아래와 같은 코드가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC7aSk/btsFnXJ0ANy/RHZ9MHLaKT2Yg97YYICDpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC7aSk/btsFnXJ0ANy/RHZ9MHLaKT2Yg97YYICDpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC7aSk/btsFnXJ0ANy/RHZ9MHLaKT2Yg97YYICDpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC7aSk%2FbtsFnXJ0ANy%2FRHZ9MHLaKT2Yg97YYICDpK%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;881&quot; height=&quot;518&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;트랜잭션을 생성할 땐 emHolder, conHolder 가 null 이었지만 여기서는 아니다. 위에 트랜잭션 리소스 등록을 보면 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;TransactionSynchronizationManager&lt;span&gt; 를 통해 리소스를 등록하는 부분이 있었다. 이 로직은 그 부분에서 등록한 엔티티 매니저 홀더와 커넥션 홀더를 가져와 트랜잭션 객체에 설정해주는 로직이다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjz0B6/btsFqKwuHja/Mzw2VYiBMFwWMyIjYXCrZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjz0B6/btsFqKwuHja/Mzw2VYiBMFwWMyIjYXCrZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjz0B6/btsFqKwuHja/Mzw2VYiBMFwWMyIjYXCrZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjz0B6%2FbtsFqKwuHja%2FMzw2VYiBMFwWMyIjYXCrZk%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;915&quot; height=&quot;336&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;트랜잭션을 생성할 땐 isExistionTransaction 이 false 라 이 로직을 안탔지만, 이번에는 트랜잭션이 존재하기 때문에 이 로직을 타게 됐다.&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;913&quot; data-origin-height=&quot;483&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVVzd6/btsFwDiKdIy/Dfk3ZeuiPj7GT6BIY5IxZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVVzd6/btsFwDiKdIy/Dfk3ZeuiPj7GT6BIY5IxZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVVzd6/btsFwDiKdIy/Dfk3ZeuiPj7GT6BIY5IxZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVVzd6%2FbtsFwDiKdIy%2FDfk3ZeuiPj7GT6BIY5IxZ1%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;913&quot; height=&quot;483&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;483&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;handlerExistingTransaction 에서는 트랜잭션에 설정된 속성에 따른 처리를 하고 마지막에 TransactionSynchronizationManager&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 의 값을 설정하고 TransactionStatus 를 반환한다. 이제 TransactionAspectSupport 의 createTransactionIfNecessary 에 있는 prepareTransactionInfo 메서드를 실행한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;573&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BMOH9/btsFm8EU6MJ/D0QUHiOsHCsgk9DE064sD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BMOH9/btsFm8EU6MJ/D0QUHiOsHCsgk9DE064sD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BMOH9/btsFm8EU6MJ/D0QUHiOsHCsgk9DE064sD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBMOH9%2FbtsFm8EU6MJ%2FD0QUHiOsHCsgk9DE064sD0%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;1020&quot; height=&quot;573&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;573&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: #333333; text-align: start;&quot;&gt;TransactionStatus&lt;span&gt; 를 설정해준다. 이제 트랜잭션 세팅이 끝났으니 쿼리를 실행하는 로직으로 넘어간다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;QueryExecutorMethodInterceptor#invoke -&amp;gt; QueryExecutorResultHandler#postProcessInvocationResult&amp;nbsp; -&amp;gt; RepositoryFactorySupport#invoke 를 호출한다. 그 이후로도 쿼리와 관련된 메서드가 호출되는 것을 확인할 수 있다. RepositoryCompository -&amp;gt; RepositoryMethodInvoker -&amp;gt; SimpleJpaRepository -&amp;gt; QueryTranslatorImpl -&amp;gt; QueryLoader -&amp;gt; Loader -&amp;gt; StatementPrepareImpl 호출이 된다. 이제야 이 훑어보기 글의 목적이 되는 부분이 나온다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;LazyConnectionDataSourceProxy&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&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;954&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWSPke/btsFvXVUoWx/dAQAziG6Z0muQP63YmIklk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWSPke/btsFvXVUoWx/dAQAziG6Z0muQP63YmIklk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWSPke/btsFvXVUoWx/dAQAziG6Z0muQP63YmIklk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWSPke%2FbtsFvXVUoWx%2FdAQAziG6Z0muQP63YmIklk%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;954&quot; height=&quot;630&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connection 을 얻어 쿼리를 실행하는 부분이다. connection 을 얻어오는 부분을 타고 들어가면 LazyConnectionDataSourceProxy 가 나온다.&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;918&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqArXb/btsFsm2XVLF/t1PlSxwQc3lQ3qWqzjVjbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqArXb/btsFsm2XVLF/t1PlSxwQc3lQ3qWqzjVjbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqArXb/btsFsm2XVLF/t1PlSxwQc3lQ3qWqzjVjbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqArXb%2FbtsFsm2XVLF%2Ft1PlSxwQc3lQ3qWqzjVjbk%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;918&quot; height=&quot;303&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgc13H/btsFoC6Jlbw/SqbsAyic7flxDbMWjhd5X0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgc13H/btsFoC6Jlbw/SqbsAyic7flxDbMWjhd5X0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgc13H/btsFoC6Jlbw/SqbsAyic7flxDbMWjhd5X0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdgc13H%2FbtsFoC6Jlbw%2FSqbsAyic7flxDbMWjhd5X0%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;829&quot; height=&quot;104&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;104&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;DataSource 를 얻고 커넥션을 얻어 오는 부분의 로직을 보면 익숙한 메서드가 보인다. 바로 determineTargetDataSource 이다. RoutingDataSourceRouter 을 정의할 때 AbstractRoutingDataSource 를 상속받아 오버라이딩한 메서드다.&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;968&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sKID6/btsFsZ7IeQs/wLw3LhIMcN0v62Kx8Z5OG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sKID6/btsFsZ7IeQs/wLw3LhIMcN0v62Kx8Z5OG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sKID6/btsFsZ7IeQs/wLw3LhIMcN0v62Kx8Z5OG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsKID6%2FbtsFsZ7IeQs%2FwLw3LhIMcN0v62Kx8Z5OG1%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;968&quot; height=&quot;276&quot; data-origin-width=&quot;968&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;트랜잭션에 readOnly 가 있으니 slave 를 반환한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1025&quot; data-origin-height=&quot;297&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br70sl/btsFoEpU5Jr/H1qVT0lT8QeOCZUAmIZYJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br70sl/btsFoEpU5Jr/H1qVT0lT8QeOCZUAmIZYJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br70sl/btsFoEpU5Jr/H1qVT0lT8QeOCZUAmIZYJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr70sl%2FbtsFoEpU5Jr%2FH1qVT0lT8QeOCZUAmIZYJ1%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;1025&quot; height=&quot;297&quot; data-origin-width=&quot;1025&quot; data-origin-height=&quot;297&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환받은 slave 를 사용해 resolvedDataSources 에서 데이터 소스를 조회한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/db89PL/btsFm5H8q65/P3VmFEH0uC7Nkvl0rdp1zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/db89PL/btsFm5H8q65/P3VmFEH0uC7Nkvl0rdp1zK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/db89PL/btsFm5H8q65/P3VmFEH0uC7Nkvl0rdp1zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdb89PL%2FbtsFm5H8q65%2FP3VmFEH0uC7Nkvl0rdp1zK%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;899&quot; height=&quot;418&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 코드에서 target 은 위에서 조회한 DataSource 를 통해 가져온 Connection 이다. Connection 에 readOnly, isolation 등을 설정한다.&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;971&quot; data-origin-height=&quot;731&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctGXAI/btsFwCqBDUh/y2jnFHZ4XE1778ThIeARq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctGXAI/btsFwCqBDUh/y2jnFHZ4XE1778ThIeARq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctGXAI/btsFwCqBDUh/y2jnFHZ4XE1778ThIeARq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctGXAI%2FbtsFwCqBDUh%2Fy2jnFHZ4XE1778ThIeARq1%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;971&quot; height=&quot;731&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;731&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection 을 사용해 쿼리를 호출하고 CrudMethodMetadataPostProcessor#invoke 를 호출하고&amp;nbsp; TransactionSynchronizationManager#unbindResource 를 호출한다. bindResource 를 통해 리소스에 등록한 데이터를 제거하는 로직이다. appRepository#findAll 메서드 호출이 끝난 이후에 서비스로 돌아온 다음, 트랜잭션이 끝날 때 cleanupTransactionInfo, commit 을 호출하며 마무리 한다.&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>Spring</category>
      <category>DB</category>
      <category>entitymanager</category>
      <category>jpa</category>
      <category>MASTER</category>
      <category>slave</category>
      <category>springboot</category>
      <category>transaction</category>
      <author>Jadie Blog</author>
      <guid isPermaLink="true">https://yongdev.tistory.com/169</guid>
      <comments>https://yongdev.tistory.com/169#entry169comment</comments>
      <pubDate>Sun, 3 Mar 2024 16:03:22 +0900</pubDate>
    </item>
    <item>
      <title>SpringBoot Master/Slave DB 세팅</title>
      <link>https://yongdev.tistory.com/168</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;SpringBoot&amp;nbsp;Master/Slave&amp;nbsp;DB&amp;nbsp;세팅&lt;/h2&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 를 하나만 사용하던 스프링 부트(버전 2.7.12) 프로젝트가 있다. 이 어플리케이션의 DB 세팅은 아래와 같이 application.properties 에 하나의 DB만 세팅 되어 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1708831928588&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.datasource.url=IP
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.username=계정
spring.datasource.password=비밀번호
spring.datasource.hikari.pool-name=풀이름
기타등등 hikari connection pool 세팅&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 를 사용해야 한다면 개발자가 직접 DataSource 를 생성하는 코드를 작성해야 한다. 또한 기존의 코드도 수정해야 할 일이 생길 수 있다. 두개의 DataSource 중에 어떤 DataSource 를 선택해야 하는지를 결정해줘야 하기 때문이다.&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;- DB 1개 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- READ 기능 : @Transactional(readOnly=true) 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- CREATE, UPDATE, DELETE 기능 :&amp;nbsp; @Transactional 사용&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;이 프로젝트의 DB 세팅을 두개로 변경해야 한다. CUD 를 수행하는 Master db와 READ 만 수행하는 Slave db 로 구성하기로 했고,&amp;nbsp; 기존에 존재하던 기능의 코드 수정은 없이 변경된 DB 세팅만 적용하고 싶다. Transactional 의 readOnly 값이 true 이면 Slave DB 를 사용하고 그 외의 @Transactional 에서는 Master DB 를 사용하고, 이러한 일을 하기 위해 &lt;b&gt;RoutingDataSource&lt;/b&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;application.properties 수정&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;일단 아래와 같이 application.properties 를 수정했다.&lt;/p&gt;
&lt;pre id=&quot;code_1708833116891&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;db.master.datasource.url=MASTER DB IP:PORT
db.master.datasource.driver-class-name=org.mariadb.jdbc.Driver
db.master.datasource.username=계정
db.master.datasource.password=비밀번호
db.master.datasource.hikari.pool-name=master-db-pool
기타등등 hikari connection pool 세팅

db.slave.datasource.url=SLAVE DB IP:PORT
db.slave.datasource.driver-class-name=org.mariadb.jdbc.Driver
db.slave.datasource.username=계정
db.slave.datasource.password=비밀번호
db.slave.datasource.hikari.pool-name=slave-db-pool
기타등등 hikari connection pool 세팅&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 정보를 입력한다.&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;DataSource 생성&lt;/h3&gt;
&lt;pre id=&quot;code_1708837907803&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Bean
    @ConfigurationProperties(prefix = &quot;db.master.datasource&quot;)
    public DataSourceProperties dbMasterDataSourceProp() {
        return new DataSourceProperties();
    }

    @Bean
    @ConfigurationProperties(prefix = &quot;db.master.datasource.hikari&quot;)
    public DataSource dbMasterDataSource() {
        return dbMasterDataSourceProp().initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean
    @ConfigurationProperties(prefix = &quot;db.slave.datasource&quot;)
    public DataSourceProperties dbSlaveDataSourceProp() {
        return new DataSourceProperties();
    }

    @Bean
    @ConfigurationProperties(prefix = &quot;db.slave.datasource.hikari&quot;)
    public DataSource dbSlaveDataSource() {
        return dbSlaveDataSourceProp().initializeDataSourceBuilder().type(HikariDataSource.class).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;RoutingDataSource 를 만들기 전에 application.properties 의 DB 정보를 읽어 DataSource 를 만들어준다.&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;DataSourceRouter 구현&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;RoutingDataSource 스프링 빈을 만들기 전에 AbstractRoutingDataSource 를 상속받은 구현체가 필요하다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708838188561&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RoutingDataSourceRouter extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
            System.out.println(&quot;================== determineCurrentLookupKey: &quot; + &quot;slave&quot;);
            return &quot;slave&quot;;
        } else {
            System.out.println(&quot;================== determineCurrentLookupKey: &quot; + &quot;master&quot;);
            return &quot;master&quot;;
        }
        // 아래와 같은 코드로 충분하나 확인을 위해 위와 같이 코딩을 했다.
        //return (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) ? &quot;slave&quot; : &quot;master&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;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;AbstractRoutingDataSource&lt;span&gt; 를 상속받고 determineCurrentLookupKey 를 오버라이딩 해야 한다. 이 메서드는 DataSource 를 고르기 위한 Key 를 제공하는 역할을 한다. 위 예시 코드에서는 트랜잭션에 readOnly 가 있으면 slave, 아니면 master 라는 문자열을 반환한다.&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: #333333; text-align: start;&quot;&gt;&lt;span&gt;RoutingDataSource 스프링 빈 생성&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1708838694411&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Bean
    @Primary // 위에서 생성한 DataSource 빈이 있기 때문에 primary 를 붙여줘야 한다.
    public DataSource routingMasterSlaveDataSource() {
        DataSource masterDataSource = dbMasterDataSource(); // 위에서 만든 Master DataSource
        DataSource slaveDataSource = dbSlaveDataSource(); // 위에서 만든 Slave DataSource

        RoutingDataSourceRouter routingDataSourceRouter = new RoutingDataSourceRouter();
        Map&amp;lt;Object, Object&amp;gt; datasourceMap = new HashMap&amp;lt;&amp;gt;() {
            {
                put(&quot;master&quot;, masterDataSource);
                put(&quot;slave&quot;, slaveDataSource);
            }
        };
        routingDataSourceRouter.setTargetDataSources(datasourceMap);
        routingDataSourceRouter.setDefaultTargetDataSource(masterDataSource);
        return routingDataSourceRouter;
    }&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: #333333; text-align: start;&quot;&gt;위에서 determineCurrentLookupKey&lt;span&gt; 메서드를 통해 Key 를 얻을 수 있다고 했다. 이 Key 가 datasourceMap 을 조회하기 위해서 사용되는 것이다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LazyConnectionDataSourceProxy 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RoutingDataSource 를 바로 사용하면 master, slave 라우팅이 안된다. 왜냐하면 스프링에서 트랜잭션을 시작할 때 트랜잭션 동기화 이전에 RoutingDataSource 빈에 디폴트로 지정해둔 master DataSource 를 통해 Connection 을 얻기 때문이다. 트랜잭션 동기화 이후에 DataSource 를 정하고 Connection 을 가져오게 하기 위해서 LazyConnectionDataSourceProxy 로 RoutingDataSource 를 감싸줘야 한다. 그러면 트랜잭션 동기화 이전에 Connection Proxy를 획득하고 이후에 readOnly 상태에 따라 DataSource 를 정하게 만들 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1708842587259&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @DependsOn(&quot;routingMasterSlaveDataSource&quot;) // routingDataSource 가 먼저 있어야 함
    @Bean
    public LazyConnectionDataSourceProxy lazyConnectionDataSource(
            @Qualifier(&quot;routingMasterSlaveDataSource&quot;) DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }&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;lazyConnectionDataSource 빈을 만들기 위해선 routingMasterSlaveDataSource 가 만들어져 있어야 하기 때문에 @DependsOn 어노테이션을 사용했다.&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;EntityManagerFactory 생성&lt;/h3&gt;
&lt;pre id=&quot;code_1708842738987&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Bean(name = &quot;dbEntityManagerFactory&quot;)
    public LocalContainerEntityManagerFactoryBean dbEntityManagerFactory(@Qualifier(&quot;lazyConnectionDataSource&quot;) DataSource dataSource) {
        HibernateJpaVendorAdapter vendor = new HibernateJpaVendorAdapter();
        vendor.setGenerateDdl(false);

        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendor);
        factory.setDataSource(dataSource);
        factory.setJpaDialect(new HibernateJpaDialect());
        factory.setPersistenceUnitName(&quot;db-em&quot;);

        Map&amp;lt;String, String&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        map.put(&quot;hibernate.show_sql &quot;, env.getProperty(&quot;db.jpa.show-sql&quot;));
        factory.setJpaPropertyMap(map);
        return factory;
    }&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: #333333; text-align: start;&quot;&gt;LazyConnectionDataSource 를 사용하는 EntityManagerFactory 를 만든다. 필요한 하이버네이트 세팅도 여기서 추가해주면 된다.&amp;nbsp;&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: #333333; text-align: start;&quot;&gt;TransactionManager 생성&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1708842847819&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Bean(name = &quot;dbTransactionManager&quot;)
    public PlatformTransactionManager dbTransactionManager(@Qualifier(&quot;dbEntityManagerFactory&quot;) EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
        jpaTransactionManager.setEntityManagerFactory(entityManagerFactory);
        jpaTransactionManager.setPersistenceUnitName(&quot;dbTransactionManager&quot;);
        return jpaTransactionManager;
    }&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;위에서 만든 EntityManagerFactory 를 통해 트랜잭션 매니저를 만들게 설정을 한다.&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;이제 MasterDB/SlaveDB 를 용도에 맞게 사용할 수 있는 설정이 마무리되었다. 이제 @Transactional(readOnly=true) 를 호출하면 determineCurrentLookupKey() 메서드에 있는 slave 로그가 찍히고, readOnly가 false 가 아닌 @Transaction 을 호출하면 master 로그가 찍히는 것을 확인할 수 있다.&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;</description>
      <category>Spring</category>
      <category>DataSource</category>
      <category>MASTER</category>
      <category>routingdatasource</category>
      <category>slave</category>
      <category>springboot</category>
      <author>Jadie Blog</author>
      <guid isPermaLink="true">https://yongdev.tistory.com/168</guid>
      <comments>https://yongdev.tistory.com/168#entry168comment</comments>
      <pubDate>Sun, 25 Feb 2024 12:42:54 +0900</pubDate>
    </item>
    <item>
      <title>Docker MariaDB 실행</title>
      <link>https://yongdev.tistory.com/167</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB CDC 프로그램을 개발하고 테스트 하기 위해 MariaDB 설치를 진행했습니다. 로컬에 설치하는 것 보다는 컨테이너를 활용하는게 나을 것 같아 도커에 MariaDB 컨테이너를 올렸습니다. Docker compose 를 통해 MariaDB 를 설치하고 외부에 만들어 놓은 Config 파일을 세팅해주는 간단한 절차만 확인해보려고 합니다. 아래와 같은 순서로 글을 작성하겠습니다. 도커는 알아서 잘 설치해주셨으리라 믿고 가겠습니다 ^_^&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker-compose 작성&lt;/li&gt;
&lt;li&gt;MariaDB Config 작성&lt;/li&gt;
&lt;li&gt;실행 및 확인&lt;/li&gt;
&lt;li&gt;추가 이슈&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker-compose 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB 컨테이너를 올리기 위해서는 도커 이미지가 필요합니다. 이미지를 받기 위해선 아래와 같이 pull 명령어를 사용하시면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt; docker pull mariadb&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;docker 이미지를 받고 docker run 명령어를 통해 실행하면 됩니다. 하지만 매번 run 명령어를 입력하고 환경변수를 추가해주는 작업이 귀찮아 docker-compose 파일로 문서화 해놓으려고 합니다. docker-compose.yml 파일 내용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;version: &quot;3.8&quot;
services:
  mysql:
    container_name: mariadb_local
    image: mariadb:10.5
    volumes:
      - ./db/conf.d:/etc/mysql/conf.d
    ports:
      - &quot;3306:3306&quot;
    environment:
      - MYSQL_ROOT_PASSWORD=12345
      - TZ=Asia/Seoul
    restart: always&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;저는 필요한 MariaDB 버전이 10.5 여서 10.5로 지정해주었습니다. 다른 명령어에 대한 설명은 스킵하고 volumes 명령어만 간단하게 설명하자면 도커 컨테이너의 파일을 사용자 시스템의 파일에 연결한다고 생각하시면 됩니다. 위 설정으로 보자면 로컬 PC의 ./db/conf.d 에 있는 설정파일을 도커 컨테이너에 있는 /etc/mysql/conf.d 와 연결한다고 보시면 됩니다. 그러면 mariadb 를 실행할 때 ./db/conf.d 에 작성해놓은 파일을 설정파일로 사용하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;environment 에 계정 설정을 추가할 수도 있고 volumes 에 sql 파일을 추가하여 db 실행시 sql 스크립트를 실행하여 테이블을 만들고 데이터를 생성할 수도 있습니다.&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;MariaDB Config 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;./db/conf.d/my.cnf 를 아래와 같이 작성해놨습니다. 저는 DB 에 binary log 세팅을 하는게 목적이었습니다. 각자 필요에 맞게 설정을 추가하시면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;  [client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
port            = 3306
#socket                 = /tmp/mysql.sock

skip-external-locking
skip-host-cache
skip-name-resolve

log-bin=mariadb-bin
binlog_format = row
binlog_row_image = full
binlog_row_metadata = full

innodb_data_file_path = ibdata1:10M:autoextend
innodb_buffer_pool_size = 16M
innodb_log_file_size = 5M
innodb_log_buffer_size = 8M
innodb_flush_log_at_trx_commit = 1
innodb_lock_wait_timeout = 50                               &lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 및 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성해놓은 파일을 기반으로 컨테이너를 실행해보겠습니다. -d 를 붙이면 백그라운드에서 실행이 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;   docker compose up -d&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;66&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpTPwu/btsEEErk98p/IDk56JbfeY2iQMztPxGy9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpTPwu/btsEEErk98p/IDk56JbfeY2iQMztPxGy9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpTPwu/btsEEErk98p/IDk56JbfeY2iQMztPxGy9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpTPwu%2FbtsEEErk98p%2FIDk56JbfeY2iQMztPxGy9k%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;66&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;66&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;컨테이너가 정상적으로 실행됐고 ./db/conf.d/my.cnf 파일에 작성한 설정이 잘 적용됐는지 확인해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;241&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baxw3r/btsEFOGHoC7/M1PaTPsQtEVJbdPQraDRgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baxw3r/btsEFOGHoC7/M1PaTPsQtEVJbdPQraDRgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baxw3r/btsEFOGHoC7/M1PaTPsQtEVJbdPQraDRgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbaxw3r%2FbtsEFOGHoC7%2FM1PaTPsQtEVJbdPQraDRgk%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;1142&quot; height=&quot;241&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;241&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1119&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mpom3/btsEE0gwywl/Zxc7RKsKrXLp4Xv1xkFyXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mpom3/btsEE0gwywl/Zxc7RKsKrXLp4Xv1xkFyXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mpom3/btsEE0gwywl/Zxc7RKsKrXLp4Xv1xkFyXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmpom3%2FbtsEE0gwywl%2FZxc7RKsKrXLp4Xv1xkFyXK%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;1119&quot; height=&quot;190&quot; data-origin-width=&quot;1119&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;my.cnf 에 설정해놓은 옵션이 잘 적용되어 있습니다. binlog_row_image 디폴트가 full 일수도 있으니 다른 옵션도 확인해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG8BOu/btsEEAJpxBS/XXsk4y6Si0QYHE9inujjj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG8BOu/btsEEAJpxBS/XXsk4y6Si0QYHE9inujjj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG8BOu/btsEEAJpxBS/XXsk4y6Si0QYHE9inujjj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG8BOu%2FbtsEEAJpxBS%2FXXsk4y6Si0QYHE9inujjj1%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;1132&quot; height=&quot;184&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;184&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;마찬가지로 잘 적용되어 있습니다. binary log 는 리눅스 기준으로 /var/lib/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;이렇게 Docker-compose 를 사용하면 내 로컬PC 에는 영향이 없고 좀 더 편하게 DB 를 실행할 수 있습니다.&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;추가1. World-writable config file 메시지&lt;/h3&gt;
&lt;pre id=&quot;code_1707633062079&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;World-writable config file '/etc/mysql/conf.d/my.cnf' is ignored&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;컨테이너에 접속 후 mariadb 에 접속을 시도할 때 위와 같은 메시지가 발생할 수 있습니다. 해당 메시지는 로컬에 만들어 놓은 설정파일이 적용되지 않았다는 의미입니다. 이 메시지를 해결하기 위해선 로컬파일의 권한 설정을 변경해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707633225423&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;chmod 755 설정파일
chmod 0444 설정파일&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 윈도우를 사용하고 있어서 아래 명령어가 실행됐지만, 리눅스 환경이시면 위 명령어도 잘 실행될 것 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가2. mysql 안먹힘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너에 접속후 mariadb 를 실행하려고 할 때, 예전에는 잘 먹혔던 mysql -u root -p 명령어가 안먹힐 수도 있습니다. 만약 MariaDB 버전이 11 이상을 사용하시는 분이라면 mysql 대신 mariadb 로 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>docker</category>
      <category>mariadb</category>
      <author>Jadie Blog</author>
      <guid isPermaLink="true">https://yongdev.tistory.com/167</guid>
      <comments>https://yongdev.tistory.com/167#entry167comment</comments>
      <pubDate>Sun, 11 Feb 2024 15:21:56 +0900</pubDate>
    </item>
    <item>
      <title>Transactional Outbox</title>
      <link>https://yongdev.tistory.com/166</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Background&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;- A 시스템 영역은 B 시스템 영역에 직접적인 의존성을 가지면 안됨 (ex: API 호출)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- A Component 에서 실행한 로직이 성공하면 결과가 &lt;b&gt;반드시&lt;/b&gt; B Component 전파되어야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- A Component 에서 실행에 실패한 로직의 결과가 B Component 로 전파되면 안됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- A Component 에서 발생한 로직의 결과는 B,C,D Component 로 전파가 될 수도 있음&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;289&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc6ZVh/btsDoquhss9/tiMOB80IvbCkmTo9d5SAIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc6ZVh/btsDoquhss9/tiMOB80IvbCkmTo9d5SAIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc6ZVh/btsDoquhss9/tiMOB80IvbCkmTo9d5SAIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc6ZVh%2FbtsDoquhss9%2FtiMOB80IvbCkmTo9d5SAIK%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;555&quot; height=&quot;289&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;289&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;시스템간 소통하는 방법은 다양한 방법이 존재합니다. 위와 같은 요구사항을 만족하기 위해 메시징 방법을 사용하기로 했고, 메시징 플랫폼 중 아파치 카프카를 사용하기로 했습니다. A 시스템에서 메시지를 발행하고 B 또는 다수의 시스템에서 구독을 해야 하기 때문입니다.&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;하지만 단순히 카프카를 사용해 메시지를 발행, 구독하는것 만으로는 모든 요구사항을 충족시키기 어렵습니다. A 영역에서 발생한 이벤트가 B 영역으로 확실하게 전달이 되어야 함을 보장해야 하고 어떻게 재시도를 할지에 대해서도 고민을 해봐야 합니다. 그리고 A 와 유사하게 발행해야 하는 시스템이 여러개 존재하고, B 와 같이 구독을 해야 하는 시스템이 다수 존재하는데,&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;&amp;nbsp; - 비즈니스 로직 실패시 메시지 발행 X&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;&amp;nbsp;비즈니스 로직과 메시지 발행을 한 트랜잭션으로 어떻게 처리해야 하는지가 고민을 하다 Transactional Outbox 패턴을 알게 되어 사용하기로 했습니다.&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;Transactional Outbox?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Outbox 의 뜻을 찾아보면 아래와 같습니다.&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;660&quot; data-origin-height=&quot;172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chzW7A/btsDqb4shq7/4s1AGDEEJ0DvxwvfQxNm0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chzW7A/btsDqb4shq7/4s1AGDEEJ0DvxwvfQxNm0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chzW7A/btsDqb4shq7/4s1AGDEEJ0DvxwvfQxNm0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchzW7A%2FbtsDqb4shq7%2F4s1AGDEEJ0DvxwvfQxNm0k%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;660&quot; height=&quot;172&quot; data-origin-width=&quot;660&quot; data-origin-height=&quot;172&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;Transactional Outbox 는 트랜잭션의 임시 보관함이라고 해석이 됩니다. 이 보관함에는 전송할 메시지나 전송에 실패한 메시지들을 보관됩니다. 임시보관함에 있는 메시지까지 전송을 완료해야 트랜잭션이 완료됩니다. 아래 그림은 Order Service 에서 비즈니스 로직을 수행하고 Order Table 에 데이터를 저장한 후에 메시지를 발행해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t0ksL/btsDnBit7YH/llH1C24Jr7SfQR5u1sXM6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t0ksL/btsDnBit7YH/llH1C24Jr7SfQR5u1sXM6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t0ksL/btsDnBit7YH/llH1C24Jr7SfQR5u1sXM6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft0ksL%2FbtsDnBit7YH%2FllH1C24Jr7SfQR5u1sXM6K%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;760&quot; height=&quot;293&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;293&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; Order Serivce 에서 Outbox Table 에 전달해야 하는 이벤트 데이터를 저장하고 Outbox Publisher 에서 Outbox Table 의 데이터를 읽어 Message Broker 로 데이터를 전달합니다. 이렇게 시스템을 구성했을 때 얻을 수 있는 장점은&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;- Order Table 에 데이터를 넣는 로직과 Outbox Table 에 이벤트를 저장하는 로직을 하나의 트랜잭션으로 묶어 비즈니스 로직과 이벤트 발행을 하나의 단위로 실행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이벤트를 Message Broker 로 전달하는 책임은 Outbox Publisher 에서만 담당하게 되니 책임이 분리되어 좀 더 알아보기 쉬운 아키텍처를 만들 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 모든 DB&amp;nbsp; 가 Outbox Table 의 스키마를 동일하게 가지게 된다면 모든 DB 의 Outbox Publisher 를 단일 소스로 구성할 수 있습니다. 관리해야 할 소스를 줄일 수 있습니다.&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;Outbox Pulisher 에서 이벤트를 읽어 처리하는 로직은 다양한 방법으로 구현할 수 있을 것 같습니다. Outbox Table 을 읽는 로직을 일정 주기마다 실행하는 스케줄러를 개발하거나 CDC 를 통해 Outbox Table 의 Insert 되는 데이터를 감지할 수도 있을 것 같습니다. 그리고 이벤트 중복 처리 방지 및 순서를 보장하는 방법은 Outbox Table 에 Sequential 하면서 고유한 트랜잭션 ID (TSID) 를 부여해 해결할 수도 있을 것 같습니다.&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;시스템 구조개선을 시작하며 Transactional Outbox Pattern 에 대해 알게 되었고 사용하기 위해 준비중입니다. 현재 시스템 상황 및 방향성과 맞는 패턴이고 앞으로 다양한 영역에서 활용할 수 있는 좋은 패턴이라 생각하여 정리해 보았습니다.&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>MSA</category>
      <category>Transactional Outbox</category>
      <category>마이크로서비스</category>
      <category>아키텍처</category>
      <author>Jadie Blog</author>
      <guid isPermaLink="true">https://yongdev.tistory.com/166</guid>
      <comments>https://yongdev.tistory.com/166#entry166comment</comments>
      <pubDate>Sun, 14 Jan 2024 19:43:37 +0900</pubDate>
    </item>
    <item>
      <title>좋은 테스트란</title>
      <link>https://yongdev.tistory.com/162</link>
      <description>&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예전부터 테스트를 작성하다보면 어떤 테스트가 좋은 테스트인지 궁금했습니다. 하지만 좋은 테스트에 대해 감히 내가 정의할 수는 없었습니다. '좋은' 이라는 단어가 상대적이어서 그럴수도 있고 단어 자체가 주는 부담감 또는 누구에게 좋은 테스트가 누구에겐 좋지 않을 수도 있는 케바케의 문제가 있을 수 있다고 생각해서 였습니다(케바케 보다는 상황에 따라서 라는 말을 더 선호합니다). 하지만 이 책에서는 좋은 테스트를 주제로 책을 시작하고 있습니다. 테스트를 왜 작성해야 하는지, 좋은 테스트란 무엇인지 쉽게 잘 설명해주어 이 글을 기반으로 정리용 글을 쓰기로 했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;테스트의 가치란?&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1. 테스트는 실수를 바로 잡아줍니다.&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;새로운 기능을 개발하면 작성한 코드가 기대했던 대로 동작하는지 확인을 해야 합니다. 그리고 기능을 리팩터링 했을때 역시 예전과 동일하게 동작하는지 확인을 해야 합니다. 테스트 코드가 없다면 코드를 하나씩 실행하면서 개발중에 실수를 했는지 확인하는 과정과 실수를 수정하는 작업이 필요합니다. 그리고 대개 이러한 작업은 한번으로 끝나지 않고 여러번 반복하고 나서야 올바른 결과물이 나오게 됩니다. 만약 실수하기 쉬워 보이는 코드에 단위 테스트를 추가한다면 기능을 올바르게 만들었는지, 리팩터링을 잘 했는지 바로 확인할 수 있고 빠르게 수정을 할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2. 테스트는 설계를 도와줍니다.&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테스트를 통해 실사용에 적합한 설계를 이끌어 낼 수 있습니다. 예를 들면 테스트 주도 개발(TDD)을 통한 개발 방법이 있을 것 같습니다. 실패하는 작은 테스트를 만들고 그 테스트를 성공할 만큼의 작은 코드를 만듭니다. 그리고 다시 실패하는 테스트를 만들고 &lt;b&gt;성공할 만큼의 작은 코드&lt;/b&gt;를 만듭니다. 이렇게 개발을 하면 실사용에 적합한 코드만 구현을 하게 되면서 좀 더 깔끔한 코딩이 가능합니다. 기능 구현에 필요한 코드만 남기 때문에 군더더기를 제거한 코드만 존재하게 됩니다. 테스트가 설계를 도와준다는 말이 TDD 의 가치라고 볼 수도 있겠지만 TDD 도 테스트가 주도하는 방법론이기 때문에 테스트가 설계를 도와준다고 말할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;하지만 무조건 테스트가 존재한다고 위에 나온 테스트 가치를 실현할 수 있는 것은 아닙니다. 잘못 만들어진 테스트 코드는 오히려 개발자 생산성에 악영향을 끼칠 수 있습니다. 극단적이고 간단한 예를 들면 아래와 같은 &lt;b&gt;사칙연산&lt;/b&gt; 계산기 테스트 코드가 있다고 해봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class CalculatorTest {
    private Calculator calculator = new Calculator();;
    @Test
    void plus() {
        assertThat(calculator.plus(1,2)).isEqualTo(3);
    }
    @Test
    void minus() {
        assertThat(calculator.minus(2,1)).isEqualTo(1);
    }
    
    static class Calculator {
        public int plus(int x, int y) {return x + y;}
        public int minus(int x, int y) {return Math.abs(x - y);}
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;동료 개발자가 사칙연산에 대한 테스트는 통과했으니 다음 개발을 하면 된다고 인수인계 해주었습니다. 순진한 우리는 그 말을 믿고 자동화된 케이스를 실행해보고 모든 테스트가 성공했다는 결과가 나오니 이어서 개발을 하지만 사실 이 사칙연산 계산기는 곱셈과 나눗셈에 대한 테스트가 작성되어 있지 않습니다(코드도 없긴 합니다). 또한 minus 테스트 메서드는 단순한 뺄셈이 아닌 값의 차이를 구하고 있기 때문에 우리가 기대했던 사칙연산 계산기의 동작과 다른 동작을 검증 하고 있습니다. 그래서 이 계산기의 테스트 코드를 믿고 개발하다가는 잘못된 길로 빠질 수 있습니다. 이 예는 단순한 예제이지만 복잡한 로직에서는 무엇이 잘못된 건지 파악하기 어려워 점점 미궁속으로 빠질 수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 테스트가 잘못 만들어졌거나 또는 테스트 케이스가 부족해 신뢰할 수 없는 테스트가 만들어지면 차라리 테스트가 없는게 좋을 수도 있습니다. 좋지 못한 테스트의 한 예로 볼 수 있습니다. 그렇다면 좋은 테스트란 뭘까요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;좋은 테스트란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 테스트 코드가 좋은 테스트코드인지 판단하는 기준은 사람마다 다를 것 입니다. 왜냐하면&amp;nbsp;&lt;b&gt;'좋다'&lt;/b&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;1.&amp;nbsp; 읽기 쉬운 코드가 유지보수도 쉽다.&lt;/h4&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;pre id=&quot;code_1698548922197&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void test() {
    int i = 10_000;
    int k = 10;
    TaxCalculator calculator = new TaxCalculator();
    assertThat(calculator.calculate(i, k)).isEqualTo(11_000);
}&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;위와 같은 테스트 코드는 테스트 메서드 이름만 봐서 무엇을 테스트 하려는지 알 수 없습니다. 그리고 테스트 내에서 사용하는 i,j 변수명도 무엇을 나타내는지 알 수 없습니다. 검증 결과 값이 무엇을 나타내는지도 명확하지 않아 가독성이 떨어지는 테스트 입니다. 메서드명, 변수명을 바꾸는 것 만으로도 가독성을 향상시킬 수 있고 BDD(Behavior Driven Delveopment)&amp;nbsp; 스타일로 작성하는 것도 고려해 볼 수 있을 것 같습니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;2. 구조화가 잘 되어 있다면 이해하기 쉽다.&lt;/h4&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 작업과 관련된 테스트 클래스를 찾을 수 있다.&lt;/li&gt;
&lt;li&gt;그 클래스에서 적절한 테스트 메서드를 고를 수 있다.&lt;/li&gt;
&lt;li&gt;그 메서드에서 사용하는 객체의 생명주기를 이해할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이점이 있기 때문에 쉽지 않겠지만 구조화된 코드를 만들어야 한다고 생각합니다.&lt;/p&gt;
&lt;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;h4 data-ke-size=&quot;size20&quot;&gt;3. 엉뚱한 걸 검사하는 건 좋지 않다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 분석할 때 테스트 코드가 존재한다면 코드 분석 전에 테스트를 먼저 실행해보곤 합니다. 테스트가 성공하면 테스트의 이름을 보며 어떤 테스트가 성공했는지 확인을 하고 다음 단계로 넘어갑니다. 여기서 메서드의 이름을 너무 믿어버리는 실수(?)를 합니다. 보통은 테스트의 이름을 보면 그 테스트가 검사하는 내용을 알 수 있기 때문에 테스트를 실행시키고 성공하는 결과만 보고 넘어갑니다. 하지만, 실제로는 이름과 전혀 관련이 없는 것을 검사하는 경우가 종종 있습니다. 우리는 테스트를 통해 특정 기능이 잘 실행된다는 것을 확신하지만, 실제로는 잘 동작하지 않는 것이므로 실제 기능이 동작할 때 문제가 생길 수 있습니다. 올바른 것을 검사하는 것 못지않게 올바른 것을 똑바로 검사하는 것도 중요합니다. 특히 유지보수 관점에서는 어떻게 구현했느냐가 아니라 의도한 대로 구현했느냐를 검사하는 것이 더 중요할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 독립적인 테스트는 혼자서도 잘 실행된다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 다른 요소에 영향을 받지 않고 독립적으로 잘 생행되어야 합니다. 요소라고 할 수 있는 것들을 예로 들면 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간&lt;/li&gt;
&lt;li&gt;임의성&lt;/li&gt;
&lt;li&gt;동시성&lt;/li&gt;
&lt;li&gt;인프라&lt;/li&gt;
&lt;li&gt;기존 데이터&lt;/li&gt;
&lt;li&gt;영속성&lt;/li&gt;
&lt;li&gt;네트워크&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 이러한 요소들과 격리 시켜야 합니다. 그렇지 않으면 테스트를 실행하고 관리하기가 힘들어지기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 하려는 코드에 외부 시스템에 환율을 조회하는 API 를 호출하는 기능이 포함되어 있다고 생각해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdprEb/btszjz9B8ar/smUfF5WDq6IAWxnENCMbV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdprEb/btszjz9B8ar/smUfF5WDq6IAWxnENCMbV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdprEb/btszjz9B8ar/smUfF5WDq6IAWxnENCMbV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdprEb%2Fbtszjz9B8ar%2FsmUfF5WDq6IAWxnENCMbV1%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;621&quot; height=&quot;145&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;145&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 서비스로부터 환율을 조회한 후 테스트를 성공합니다. 하지만 만약에 외부 서비스에 이상이 생겨 환율 API 호출이 실패한다면 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;609&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1kYj0/btszkyI73H2/v4M0PqOfnLKnTtdQNZ0lG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1kYj0/btszkyI73H2/v4M0PqOfnLKnTtdQNZ0lG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1kYj0/btszkyI73H2/v4M0PqOfnLKnTtdQNZ0lG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1kYj0%2FbtszkyI73H2%2Fv4M0PqOfnLKnTtdQNZ0lG0%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;609&quot; height=&quot;132&quot; data-origin-width=&quot;609&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;b&gt;테스트 더블(Test Double) &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;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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고 : Effective Unit Testing(한빛미디어)의 1,2 장&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Keyword : TDD, BDD, 테스트 더블&lt;/p&gt;</description>
      <category>TDD,Test</category>
      <category>Test</category>
      <author>Jadie Blog</author>
      <guid isPermaLink="true">https://yongdev.tistory.com/162</guid>
      <comments>https://yongdev.tistory.com/162#entry162comment</comments>
      <pubDate>Sun, 29 Oct 2023 15:11:19 +0900</pubDate>
    </item>
    <item>
      <title>OAS 사용해 API 문서 작성하기</title>
      <link>https://yongdev.tistory.com/164</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 주로 spring-restdocs 를 이용해 API 를 문서화 했습니다. spring-restdocs 를 사용한 이유는 아래와 같은 이유로 사용을 했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드를 기반으로 작성, 테스트를 통과해야 문서를 만들 수 있으니 문서에 신뢰성이 생깁니다&lt;/li&gt;
&lt;li&gt;프로덕션 코드에 문서화를 위한 코드를 추가할 필요가 없습니다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swagger 는 문서화를 위한 코드를 추가해줘야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유로 restdocs 를 사용했지만 문제가 있었습니다. 바로 문서를 보기 싫다는것! (제 기준으로는...ㅎㅎ) 기껏 신경써서 문서를 만들었지만 화면에 보이는 것은 칙칙한 API 문서&amp;hellip; 조치가 필요했습니다. 그래서 Swagger 사용을 고려해봤으나 코드에 api 문서 관련 코드를 넣고 싶지 않아 사용하지 않았습니다. 자료를 찾아보니 테스트 코드를 기반으로 문서화를 하지만 문서는 Swagger 형식으로 이쁘게 출력할 수 있는 방법이 존재했습니다.&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;바로 OAS(OpenAPI Spec) 을 사용하는 것 입니다. 테스트 코드를 기반으로 API Spec 을 JSON 이나 YAML 파일로 표현하고, 해당 파일을 Swagger 형식으로 출력하는 방법입니다. 회사 프로젝트가 maven 을 사용하기 때문에 maven 빌드 툴에서 어떻게 문서화를 했는지 보겠습니다. (구글링을 하면 gradle 빌드 툴을 사용한 예제 많이 나옵니다. )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발환경&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDK 17&lt;/li&gt;
&lt;li&gt;인텔리제이&lt;/li&gt;
&lt;li&gt;Spring Boot 2.7.9&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;의존성 추가&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 문서를 만들기 위해서 pom.xml에 아래 의존성을 추가해줘야 합니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.restdocs&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-restdocs-restassured&amp;lt;/artifactId&amp;gt;
    &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
		&amp;lt;groupId&amp;gt;com.epages&amp;lt;/groupId&amp;gt;
		&amp;lt;artifactId&amp;gt;restdocs-api-spec&amp;lt;/artifactId&amp;gt;
		&amp;lt;version&amp;gt;0.16.4&amp;lt;/version&amp;gt;
		&amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.epages&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;restdocs-api-spec-restassured&amp;lt;/artifactId&amp;gt;
		&amp;lt;version&amp;gt;0.16.4&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;코드 작성&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;샘플용 API 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성을 추가하고 문서로 만들 샘플용 API 를 만들었습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@PostMapping(&quot;/intsvc/homepage/payment_kakaopay/v1/payment&quot;)
public ApiResponse&amp;lt;?&amp;gt; paymentRequest(@RequestBody PaymentRequest req) {
    return ApiResponse.of(KakaopayApiCode.SUCCESS.getCode(), &quot;정상적으로 처리하였습니다.&quot;, service.paymentRequest(req));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 설명을 드리면 paymentRequest 는 kakaopay 결제를 요청하는 API 이고, 아래 3개의 API 는 고객이 카카오페이에 결제수단을 등록하고 나면 실행되는 결제 승인,취소,실패를 처리하는 API 입니다. 우선 첫번째 API 로 테스트 코드를 만들어 실행해보겠습니다. 테스트 코드 전체를 보시려면 하단의 git 주소로 가서 보시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 코드 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 테스트 코드에 어노테이션을 추가해줘야 합니다&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@ActiveProfiles(&quot;test&quot;)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith({RestDocumentationExtension.class})
public class PaymentRequestApiDocs { 
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@WebMvcTest 를 사용해도 컨트롤러 영역의 테스트, 문서화를 할 수 있다고 합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@DisplayName(&quot;결제요청 API&quot;)
@Test
void success() {
    카카오결제준비API요청Mock(&quot;payment-request.json&quot;); // (1)

    ExtractableResponse response = 결제요청(); // (2)

    String rescd = response.jsonPath().getObject(&quot;rescd&quot;, String.class); // (3)
    assertThat(rescd).isEqualTo(&quot;00&quot;);
    PaymentResponse resultInfo = response.jsonPath().getObject(&quot;resultInfo&quot;, PaymentResponse.class);
    assertThat(resultInfo.pcUrl()).isEqualTo(&quot;&amp;lt;https://online-pay.kakao.com/mockup/v1/69137840d93f62a9738b03b0fd000ab914302e894b0d9c59ae3a2a6702c2d33f/info&amp;gt;&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 요청에 필요한 객체를 만들고, API 요청을 하고, 응답이 예상했던 값과 일치하는지 비교하는 코드 입니다. 먼저 문서화와는 무관하지만 테스트 코드 부분을 설명 드리겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1) : 결제를 요청하기 위해선 카카오페이 결제준비API 를 호출해야 합니다. 하지만 테스트 코드에서는 실제로 카카오페이의 결제준비 API 를 호출할 필요는 없습니다. 외부환경에 의존하지 않는 테스트 코드를 만들기 위해서 Wiremock 을 사용해서 api 호출을 대체했습니다.&lt;/li&gt;
&lt;li&gt;(2) : 결제요청 API를 호출하는 메서드 입니다. 또한 문서 작성용 코드가 포함 되어 있습니다.&lt;/li&gt;
&lt;li&gt;(3) : API 응답 결과와 예상했던 값이 일치하는지 비교하는 코드 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 문서화 하는 코드가 있는 (2) 메서드를 확인해봅시다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;private Snippet REQUEST_FIELDS = requestFields(
        fieldWithPath(&quot;orderId&quot;).type(JsonFieldType.NUMBER).description(&quot;주문번호&quot;),
        fieldWithPath(&quot;userId&quot;).type(JsonFieldType.NUMBER).description(&quot;회원ID&quot;),
        fieldWithPath(&quot;productName&quot;).type(JsonFieldType.STRING).description(&quot;상품이름&quot;),
        fieldWithPath(&quot;quantity&quot;).type(JsonFieldType.NUMBER).description(&quot;수량&quot;),
        fieldWithPath(&quot;amount&quot;).type(JsonFieldType.NUMBER).description(&quot;결제금액&quot;),
        fieldWithPath(&quot;paymentMethodType&quot;).type(JsonFieldType.STRING).description(&quot;결제수단(MONEY,CARD)&quot;),
        fieldWithPath(&quot;installMonth&quot;).type(JsonFieldType.NUMBER).description(&quot;할부개월&quot;),
        fieldWithPath(&quot;taxFreeAmount&quot;).type(JsonFieldType.NUMBER).optional().description(&quot;비과세금액&quot;)
        );

private Snippet RESPONSE_FIELDS = responseFields(apiResponseFields())
        .andWithPrefix(&quot;resultInfo.&quot;,
                fieldWithPath(&quot;payId&quot;).type(JsonFieldType.NUMBER).description(&quot;결제ID&quot;),
                fieldWithPath(&quot;tid&quot;).type(JsonFieldType.STRING).description(&quot;결제고유번호&quot;),
                fieldWithPath(&quot;orderId&quot;).type(JsonFieldType.NUMBER).description(&quot;주문번호&quot;),
                fieldWithPath(&quot;payAmount&quot;).type(JsonFieldType.NUMBER).description(&quot;금액&quot;),
                fieldWithPath(&quot;pcUrl&quot;).type(JsonFieldType.STRING).description(&quot;PC URL&quot;)
        );

...

protected RequestSpecification spec;

@BeforeEach
void setUpRestdocs(RestDocumentationContextProvider restDocumentation) {
    this.spec = new RequestSpecBuilder()
            .setPort(port)
            .setConfig(RestAssuredConfig.config()
                    .encoderConfig(EncoderConfig.encoderConfig().defaultCharsetForContentType(&quot;UTF-8&quot;, ContentType.JSON))
                    .decoderConfig(DecoderConfig.decoderConfig().defaultCharsetForContentType(&quot;UTF-8&quot;, ContentType.JSON))
            )
            .addFilter(
                    documentationConfiguration(restDocumentation)
                            .operationPreprocessors()
                            .withRequestDefaults(prettyPrint())
                            .withResponseDefaults(prettyPrint())
            )
            .build();
}

ResourceSnippetDetails resourceSnippetParametersBuilder = new ResourceSnippetParametersBuilder();
ExtractableResponse&amp;lt;Response&amp;gt; response =
        given(this.spec)
                .filter(
                        RestAssuredRestDocumentationWrapper.document(
                                &quot;결제요청 Identifier&quot;
                                , resourceSnippetParametersBuilder
                                        .tag(&quot;결제요청&quot;)
                                        .summary(&quot;결제요청 summary&quot;)
                                        .description(&quot;카카오페이 결제 요청을 위한 API&quot;)
                                        .requestSchema(Schema.schema(&quot;PaymentRequest&quot;))
                                        .responseSchema(Schema.schema(&quot;ApiResponse&amp;lt;PaymentResponse&amp;gt;&quot;))
                                , REQUEST_FIELDS, RESPONSE_FIELDS) // 스니펫정의
                )
                .accept(MediaType.APPLICATION_JSON_VALUE)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .body(req)
                .log().all()
                .when()
                .post(&quot;/intsvc/homepage/payment_kakaopay/v1/payment&quot;)
                .then()
                .log().all()
                .extract();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서화의 핵심 코드는 .filter (&amp;hellip;) 부분입니다. 이 부분에서 문서에 적힐 내용을 정의하는 것이기 때문입니다. tag, summary, description, requestSchema, responseSchema 에 API 에 대한 설명을 적으면 되고 REQUEST_FIELDS, RESPONSE_FIELDS 부분에 API 호출의 요청 및 응답 객체를 정의하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;spec 정의와 filter 부분을 제외하면 RestAssured 를 통한 api 테스트 코드와 동일한 형태입니다.&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;테스트를 실행하고 통과를 하면 target 디렉토리에 generated-snippets 에 api 문서 작성에 필요한 adoc 파일들이 생성됩니다.&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-filename=&quot;1.png&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bS8f79/btsxtoHVfTC/3V2XP3xlBaU89Cuj8lfXmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bS8f79/btsxtoHVfTC/3V2XP3xlBaU89Cuj8lfXmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bS8f79/btsxtoHVfTC/3V2XP3xlBaU89Cuj8lfXmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbS8f79%2FbtsxtoHVfTC%2F3V2XP3xlBaU89Cuj8lfXmK%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;270&quot; height=&quot;320&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;플러그인 추가&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Rest Docs 를 이용해 문서를 만들때도 이러한 파일들이 생깁니다. 이제 OpenAPI Spec을 만들어주는 플로그인을 pom.xml 에 추가해줍니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;plugin&amp;gt;
    &amp;lt;groupId&amp;gt;io.github.berkleytechnologyservices&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;restdocs-spec-maven-plugin&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;${restdocs-spec.version}&amp;lt;/version&amp;gt;
    &amp;lt;executions&amp;gt;
        &amp;lt;execution&amp;gt;
            &amp;lt;goals&amp;gt;
                &amp;lt;goal&amp;gt;generate&amp;lt;/goal&amp;gt;
            &amp;lt;/goals&amp;gt;
            &amp;lt;configuration&amp;gt;
                &amp;lt;specification&amp;gt;OPENAPI_V3&amp;lt;/specification&amp;gt;&amp;lt;!-- switch this to POSTMAN_COLLECTION for Postman Collection specs --&amp;gt;
                &amp;lt;format&amp;gt;JSON&amp;lt;/format&amp;gt;
                &amp;lt;host&amp;gt;localhost:8080&amp;lt;/host&amp;gt;
                &amp;lt;snippetsDirectory&amp;gt;${project.build.directory}/generated-snippets&amp;lt;/snippetsDirectory&amp;gt;
                &amp;lt;outputDirectory&amp;gt;${project.build.directory}/classes/static/kakaopay/swagger-ui&amp;lt;/outputDirectory&amp;gt;
            &amp;lt;/configuration&amp;gt;
        &amp;lt;/execution&amp;gt;
    &amp;lt;/executions&amp;gt;
&amp;lt;/plugin&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;generated-snippet 디렉터리에 생성된 파일을 사용해서 &amp;lt;outputDirectory&amp;gt; 디렉터리에 OpenAPI 스펙의 JSON 파일을 만드는 설정입니다.이제 maven package 를 실행하면 문서화를 위한 json 파일이 만들어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 저는 단순히 패키지를 실행하는 것 만으로는 문서화 작업이 잘 안됐습니다. 원인을 파악해보니 문서화를 위해 필요한 테스트 코드를 실행하지 않았기 때문입니다. 그래서 아래와 같이 플러그인을 하나 더 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;plugin&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;maven-surefire-plugin&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.22.0&amp;lt;/version&amp;gt;
    &amp;lt;configuration&amp;gt;
        &amp;lt;encoding&amp;gt;UTF-8&amp;lt;/encoding&amp;gt;
        &amp;lt;includes&amp;gt;
            &amp;lt;include&amp;gt;**/*Docs.*&amp;lt;/include&amp;gt;
            &amp;lt;include&amp;gt;**/*Test.*&amp;lt;/include&amp;gt;
        &amp;lt;/includes&amp;gt;
    &amp;lt;/configuration&amp;gt;
&amp;lt;/plugin&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인 추가 후 터미널창이나 IDE 의 maven package 를 실행하면 아래 사진과 같은 파일이 생성됩니다.&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-filename=&quot;2.png&quot; data-origin-width=&quot;334&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1BJml/btsxqzXJ8jH/NwXaj4ySDoubKdeNrcx9Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1BJml/btsxqzXJ8jH/NwXaj4ySDoubKdeNrcx9Qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1BJml/btsxqzXJ8jH/NwXaj4ySDoubKdeNrcx9Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1BJml%2FbtsxqzXJ8jH%2FNwXaj4ySDoubKdeNrcx9Qk%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;334&quot; height=&quot;382&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;334&quot; data-origin-height=&quot;382&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;openapi-3.0.json 만들어졌습니다. 해당 파일을 열어보면 api 에 대한 정보가 적혀있습니다. OpenAPI Spec JSON 파일을 만들었으니 SwaggerUI 를 적용해 문서화를 하는 작업을 해봅시다.&lt;/p&gt;
&lt;h1&gt;UI 설정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Swagger 다운로드&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://swagger.io/docs/open-source-tools/swagger-ui/usage/installation/&quot;&gt;https://swagger.io/docs/open-source-tools/swagger-ui/usage/installation/&lt;/a&gt; 접속&lt;/li&gt;
&lt;li&gt;화면 하단에 Download the latest release 클릭
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현재 최신 버전은 아래 링크를 통해 받으시면 됩니다&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/swagger-api/swagger-ui/releases/tag/v4.18.2&quot;&gt;https://github.com/swagger-api/swagger-ui/releases/tag/v4.18.2&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;sourcecode.zip 다운로드 후 압축 해제&lt;/li&gt;
&lt;li&gt;프로젝트의 resources 디렉터리 하위에 static 디렉터리를 만들고 dist 디렉터리를 통째로 복사&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Swagger 설정 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복사가 끝나면 설정을 시작해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. /static/dist/index.html 의 이름을 /static/dist/pay-api-docs.html 로 변경해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. swagger-initializer.js에 있는 SwaggerUIBundle url 을 새롭게 생성된 openapi-3.0.json 파일의 경로로 바꿔줍니다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;url: &quot;&amp;lt;http://localhost:8080/static/kakaopay/swagger-ui/openapi-3.0.json&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정적 리소스 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 파일을 읽을 수 있게 설정을 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler(&quot;/static/**&quot;).addResourceLocations(&quot;classpath:/static/&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;결과&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모든 설정이 끝났습니다. maven package 를 실행하고 어플리케이션을 실행한다음에 문서를 호출해봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;345&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OSBrq/btsxrjmM5ME/6khku0OjfUD0K6fYO7Iw3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OSBrq/btsxrjmM5ME/6khku0OjfUD0K6fYO7Iw3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OSBrq/btsxrjmM5ME/6khku0OjfUD0K6fYO7Iw3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOSBrq%2FbtsxrjmM5ME%2F6khku0OjfUD0K6fYO7Iw3K%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;710&quot; height=&quot;345&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;345&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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-filename=&quot;4.png&quot; data-origin-width=&quot;695&quot; data-origin-height=&quot;453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJ0NvI/btsxLiNExWF/gCTVK1mMXMCyJ9INhwjjA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJ0NvI/btsxLiNExWF/gCTVK1mMXMCyJ9INhwjjA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJ0NvI/btsxLiNExWF/gCTVK1mMXMCyJ9INhwjjA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJ0NvI%2FbtsxLiNExWF%2FgCTVK1mMXMCyJ9INhwjjA0%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;695&quot; height=&quot;453&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;695&quot; data-origin-height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 작성하며 입력했던 내용들이 화면 어디에 출력되는지 확인해 봅시다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;.tag(&quot;결제요청&quot;)
.summary(&quot;결제요청 summary&quot;)
.description(&quot;카카오페이 결제 요청을 위한 API&quot;)
.requestSchema(Schema.schema(&quot;PaymentRequest&quot;))
.responseSchema(Schema.schema(&quot;ApiResponse&amp;lt;PaymentResponse&amp;gt;&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAPI Spec 을 사용해 Swagger UI 형식의 API 문서를 만드는 방법을 알아봤습니다.&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;참고&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/BerkleyTechnologyServices/restdocs-spec&quot;&gt;https://github.com/BerkleyTechnologyServices/restdocs-spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/BerkleyTechnologyServices/restdocs-spec-example&quot;&gt;https://github.com/BerkleyTechnologyServices/restdocs-spec-example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://swagger.io/docs/specification/about/&quot;&gt;https://swagger.io/docs/specification/about/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ePages-de/restdocs-api-spec&quot;&gt;https://github.com/ePages-de/restdocs-api-spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.kakaopay.com/post/openapi-documentation/&quot;&gt;https://tech.kakaopay.com/post/openapi-documentation/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yongc.tistory.com/18&quot;&gt;https://yongc.tistory.com/18&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://taetaetae.github.io/posts/a-combination-of-swagger-and-spring-restdocs/&quot;&gt;https://taetaetae.github.io/posts/a-combination-of-swagger-and-spring-restdocs/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>API문서</category>
      <category>OAS</category>
      <category>Spring</category>
      <category>swagger</category>
      <author>Jadie Blog</author>
      <guid isPermaLink="true">https://yongdev.tistory.com/164</guid>
      <comments>https://yongdev.tistory.com/164#entry164comment</comments>
      <pubDate>Mon, 9 Oct 2023 23:26:58 +0900</pubDate>
    </item>
    <item>
      <title>Resilience4jFeign Java Record 문제</title>
      <link>https://yongdev.tistory.com/163</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Resilience4jFeign 을 사용해 Content-Type 이 x-www-form-urlencoded 인 API 를 호출할 때 요청 객체를 Record 로 선언하면 발생하는 문제입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 시스템을 구현하던 중에 발생한 이슈입니다. 새 결제 시스템 프로젝트 구성은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java 17&lt;/li&gt;
&lt;li&gt;Spring boot 2.7.9&lt;/li&gt;
&lt;li&gt;resilience4j-feign 2.0.2&lt;/li&gt;
&lt;li&gt;feign-(core,okhttp,jackson,slf4j) 12.2&lt;/li&gt;
&lt;li&gt;feign-form 3.8.0&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 환경을 구성해 &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaopay/single-payment#prepare&quot;&gt;카카오페이 결제준비 API&lt;/a&gt; 를 호출할 때 발생한 이슈에 대해서 기록해 놓으려고 합니다. 회사 프로젝트를 올릴 순 없어 해당 이슈 기록을 위한 작은 프로젝트를 하나 만들었습니다. 아래 코드는 이슈를 설명하기 위해 필요한 코드만 적어놨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public record KakaopayReadyApiRequest(
        String cid,
        String partner_order_id,
        String partner_user_id,
        String item_name,
        Integer quantity,
        Integer total_amount,
        String approval_url,
        String cancel_url,
        String fail_url,
        String payment_method_type,
        Integer install_month
) {
    @Builder
    public KakaopayReadyApiRequest {}
}

@Headers(value = {&quot;Authorization: KakaoAK {Authorization}&quot;, &quot;Accept: application/json&quot;, &quot;Content-Type: application/x-www-form-urlencoded&quot;})
public interface KakaopayApiClient {

    @RequestLine(&quot;POST /v1/payment/ready&quot;)
    KakaopayReadyApiResponse ready(@Param(&quot;Authorization&quot;) String adminKey, KakaopayReadyApiRequest request);
}

@Bean
public KakaopayApiClient kakaopayApiClient() {
    CircuitBreaker cb = CircuitBreaker.of(&quot;kakaopayApiClientCB&quot;,
            CircuitBreakerConfig.custom()
                    .failureRateThreshold(50)
                    .slidingWindow(20, 20, CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
                    .permittedNumberOfCallsInHalfOpenState(10)
                    .waitDurationInOpenState(Duration.ofSeconds(10L))
                    .build()
    );

    FeignDecorators decorators = FeignDecorators.builder()
            .withCircuitBreaker(cb)
            .build();

    ObjectMapper objectMapper = (new ObjectMapper())
            .configure(SerializationFeature.INDENT_OUTPUT, true)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .registerModules(new JavaTimeModule());

    return Resilience4jFeign.builder(decorators)
            .client(new OkHttpClient())
            .encoder(new FormEncoder())
            .decoder(new JacksonDecoder(objectMapper))
            .options(new Request.Options(3000, TimeUnit.MILLISECONDS, 10000, TimeUnit.MILLISECONDS, true))
            // 0.1초 간격으로 시작해 최대 3초의 간격으로 3번 재시도
            .retryer(new Retryer.Default(100, SECONDS.toMillis(3), 3))
            .logger(new Slf4jLogger(KakaopayApiClient.class))
            .target(KakaopayApiClient.class, url);
}
&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;위와 같이 Resilience4jFeign 을 사용해서 api 클라이언트를 만들었습니다. 다른 옵션들에 대한 설명은 제외하고 이번 이슈에 필요한 부분만 설명 하겠습니다. 이번 이슈는 &lt;b&gt;encoder&lt;/b&gt; 옵션과 관련이 있습니다. &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaopay/single-payment#prepare&quot;&gt;카카오페이 결제준비 API&lt;/a&gt;는 문서에서 확인할 수 있듯이 API 요청시 &lt;b&gt;Content-Type&lt;/b&gt; 을 &lt;b&gt;application/x-www-form-urlencoded&lt;/b&gt; 형태로 보내야 합니다. 그래서 encoder 에 &lt;b&gt;feign-form&lt;/b&gt;의 &lt;b&gt;FormEncoder&lt;/b&gt; 를 사용했습니다. 위와 같이 구성하고 API 를 호출하면 아래와 같은 에러가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;br /&gt;Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.FeignException$BadRequest: [400 Bad Request] during [POST] to [&lt;br /&gt;https://kapi.kakao.com/v1/payment/ready&lt;br /&gt;] [KakaopayApiClient#ready(String,KakaopayReadyApiRequest)]: [{&quot;msg&quot;:&quot;cid can't be null.&quot;,&quot;code&quot;:-2}]] with root cause&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;요청 객체에 cid 를 넣었음에도 카카오페이 결제준비 API 호출 응답에서는 cid가 null이 되면 안된다는 응답이 왔습니다. 그래서 혹시 인코딩이 잘 안됐나 싶어서 api 요청시 인코딩을 담당하는 FormEncoder 를 추적 해보기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FormEncoder 에 encode 메서드를 따라가다 보면 PojoUtil.toMap(object) 코드가 있습니다. toMap 메서드는 API 요청 객체인 KakaopayReadyApiRequest 를 Map 으로 변환하는 코드 입니다. 그런데 변환할때 문제가 되는건 아래 코드 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;for(int var6 = 0; var6 &amp;lt; var5; ++var6) {
    Field field = var4[var6];
    int modifiers = field.getModifiers();
    if (!Modifier.isFinal(modifiers) &amp;amp;&amp;amp; !Modifier.isStatic(modifiers)) {
        setAccessibleAction.setField(field);
        AccessController.doPrivileged(setAccessibleAction);
        Object fieldValue = field.get(object);
        if (fieldValue != null) {
            String propertyKey = field.isAnnotationPresent(FormProperty.class) ? ((FormProperty)field.getAnnotation(FormProperty.class)).value() : field.getName();
            result.put(propertyKey, fieldValue);
        }
    }
}
&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;KakaopayReadyApiRequest 의 필드를 하나씩 읽어 Map 에 넣는 코드인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if !(Modifiers.isFinal(modifiers) 와 !Modifier.isStatic(modifiers)) 을 보면 필드에 final 이 있을때 해당 로직을 실행하지 않게 되어 있습니다. KakaopayReadyApiRequest 객체는 record 로 선언이 되어 있기에 이 객체의 필드는 final 로 선언이 되어 있습니다.(Record 타입 필드엔 final 이 붙습니다) final 로 선언이 되어 있기 때문에 if 문 안에 있는 로직을 실행하지 않고 map 에는 아무 데이터도 들어가지 않게 됩니다. 그래서 해당 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;이 문제를 해결하기 위해 요청 객체를 아래와 같이 record &amp;rarr; class 로 변경했습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class KakaopayReadyApiRequest {
    private String cid;
    private String partner_order_id;
    private String partner_user_id;
    private String item_name;
    private Integer quantity;
    private Integer total_amount;
    private String approval_url;
    private String cancel_url;
    private String fail_url;
    private String payment_method_type;
    private Integer install_month;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;class 로 변경하니 PojoUtil.toMap(object) 의 Map에 데이터가 잘 들어가고 요청 또한 잘 전달되는 것을 확인할 수 있었습니다. 이 이슈는 전에 Content-Type 을 json 타입으로 전송할때는 Record 로 전송이 잘 됐어서 이번에 URLENCODED 로 전송할 때도 잘 되겠거니 생각했던게 문제였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reference Keyword&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Resilience4j&lt;/li&gt;
&lt;li&gt;Feign&lt;/li&gt;
&lt;li&gt;Java Record&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>java</category>
      <category>record</category>
      <category>Resilience4jFeign</category>
      <author>Jadie Blog</author>
      <guid isPermaLink="true">https://yongdev.tistory.com/163</guid>
      <comments>https://yongdev.tistory.com/163#entry163comment</comments>
      <pubDate>Mon, 9 Oct 2023 23:18:19 +0900</pubDate>
    </item>
    <item>
      <title>Notion 에서 이동중</title>
      <link>https://yongdev.tistory.com/161</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Notion에서 티스토리로 이동중&lt;/p&gt;</description>
      <author>Jadie Blog</author>
      <guid isPermaLink="true">https://yongdev.tistory.com/161</guid>
      <comments>https://yongdev.tistory.com/161#entry161comment</comments>
      <pubDate>Mon, 9 Oct 2023 12:14:53 +0900</pubDate>
    </item>
    <item>
      <title>카프카 토픽과 파티션</title>
      <link>https://yongdev.tistory.com/160</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아파치 카프카 - 애플리케이션 프로그래밍 (최원영 지음) 4장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.1 토픽과 파티션&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1.1 적정 파티션 개수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽의 파티션 개수는 성능과 관련이 있다. 그래서 파티션 개수를 정할땐 신중히 정해야 한다. 파티션 개수 고려사항은 아래 3개가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 처리량&lt;/li&gt;
&lt;li&gt;메시지 키 사용 여부&lt;/li&gt;
&lt;li&gt;브로커, 컨슈머 영향도&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 처리량&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션은 카프카 병렬처리의 핵심 요소이다. 파티션의 개수가 많아지면 컨슈머와 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;컨슈머 처리량을 늘리는 방법은 Scale up, GC 튜닝 등이 있지만 일정 수준 이상 처리량을 올리기 어렵다. 반면 파티션 개수를 늘리는 방법은 데이터 처리 속도를 올릴 수 있는 확실한 방법이다. 프로듀서가 보내는 데이터양과 컨슈머 데이터 처리량을 계산해 파티션 개수를 정하면 된다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;프로듀서 전송 데이터량 &amp;lt; 컨슈머 데이터 처리량 * 파티션 개수
EX)      1000개       &amp;lt;         100개       *     최소10개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 데이터 처리량을 구할 때는 운영중인 카프카에서 더미 데이터로 테스트해야 한다. 컨슈머는 다른 시스템과 연동될 수 있기 때문에 로컬 또는 테스트 환경에서 진행하면 운영 환경과 달라 데이터 처리량에 차이가 있을 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메시지 키 사용 여부&lt;/h4&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-filename=&quot;edited_1.png&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eAnAsk/btsxg2zANly/afCoHDmZq2kTrnZFPW1Rx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eAnAsk/btsxg2zANly/afCoHDmZq2kTrnZFPW1Rx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eAnAsk/btsxg2zANly/afCoHDmZq2kTrnZFPW1Rx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeAnAsk%2Fbtsxg2zANly%2FafCoHDmZq2kTrnZFPW1Rx1%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;690&quot; height=&quot;245&quot; data-filename=&quot;edited_1.png&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h4 data-ke-size=&quot;size20&quot;&gt;브로커, 컨슈머 영향도&lt;/h4&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;4.1.2 토픽 정리 정책&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽의 데이터는 시간, 용량에 따라 삭제 규칙을 정할 수 있다. 데이터를 삭제하지 않으면 추후에 데이터가 필요할 때 오프셋을 지정해서 지난 데이터를 가져올 수 있다. 또한 영구 보존할 수도 있지만 저장소 사용량이 계속 늘어나게 된다. 토픽 정리 정책엔 2가지가 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;토픽 삭제 정책(delete policy)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽의 데이터를 삭제하는 정책이다. 토픽의 데이터를 삭제할 땐 세그먼트 단위로 삭제를 진행한다. 세그먼트는 토픽의 데이터를 저장하는 명시적인 파일 시스템 단위이다. 세그먼트는 파티션마다 별개로 생성되며 세그먼트의 파일 이름은 오프셋 중 가장 작은 값이 된다. segment.bytes 옵션으로 크기를 지정할 수 있고, 지정한 크기보다 커질 경우엔 기존 세그먼트 파일을 닫고 새로운 세그먼트를 열어 데이터를 저장한다. 데이터를 저장하기 위해 사용중인 세그먼트를 액티브 세그먼트라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제정책 실행 시점은 시간 또는 용량이 기준이 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간 : 일정 주기마다 세그먼트 파일의 마지막 수정 시간과 retention.ms 값을 비교하는데, 세그먼트 파일의 마지막 수정 시간이 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;retention.ms&lt;span&gt; &lt;/span&gt;&lt;/span&gt;를 넘어가면 세그먼트 삭제한다&lt;/li&gt;
&lt;li&gt;용량 : retention.bytes를 넘어간 세그먼트 파일을 삭제한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;토픽 압축 정책(compact policy)&lt;/h4&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z8MqI/btsxrXXVQek/ORspaDrE8YQUGd9559SPSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z8MqI/btsxrXXVQek/ORspaDrE8YQUGd9559SPSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z8MqI/btsxrXXVQek/ORspaDrE8YQUGd9559SPSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ8MqI%2FbtsxrXXVQek%2FORspaDrE8YQUGd9559SPSk%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;441&quot; height=&quot;284&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;284&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;h3 data-ke-size=&quot;size23&quot;&gt;4.1.3 ISR(In-Sync-Replicas)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISR 은 리더 파티션과 팔로워 파티션이 모두 싱크가 된 상태를 뜻한다. 리더 파티션에 0~3의 오프셋이 있다고 할때, 팔로워 파티션에 동기화가 완료되려면 0~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;프로듀서가 특정 파티션에 데이터를 저장하는 작업은 리더 파티션을 통해 처리한다. 이때 리더 파티션에 새로운 레코드가 추가되어 오프셋이 증가하면, 팔로워 파티션이 위치한 브로커는 리더 파티션의 데이터를 복제한다. 팔로워 파티션이 리더 파티션으로부터 데이터를 복제하는 시간차 때문에 오프셋 차이가 발생할 수 있고, 이런 차이를 모니터링 하기 위해 리터 파티션은 replica.lag.time.max.ms값 만큼의 주기를 가지고 팔로워 파티션이 데이터를 복제하는지 확인한다. 만약 설정한 값보다 긴시간 동안 데이터를 가져가지 않는다면 팔로워 파티션에 문제가 생긴것으로 간주하고 ISR 그룹에서 제외한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ISR로 묶인 팔로워 파티션은 리더에 문제가 생기면 리더로 선출되도록 설정할 수 있다. 리더 파티션이 있는 브로커의 문제가 해결될때까지 기다리는 방법도 있지만, 이때 해당 토픽은 사용할 수 없다. 때문에 데이터 유실은 발생하지 않지만 서비스 운영에 문제가 생긴다.&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;unclean.leader.election.enable을 true 로 설정하면 ISR이 아닌 팔로워 파티션, 즉 동기화가 되지 않는 파티션이 리더로 선출될 수 있다. 이때 서비스 운영에 문제는 안생기지만, 동기화가 완벽히 되지 않은 팔로워 파티션이 리더로 선출되기 때문에 데이터 유실이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.2 카프카 프로듀서&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2.1 acks 옵션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;acks 옵션을 통해 프로듀서가 전송한 데이터가 카프카 클러스터에 얼마나 신뢰성 높게 저장할지 지정할 수 있다. 그리고 acks 옵션에 따라 성능이 달라질 수 있으므로 acks 옵션에 따른 카프카의 동작방식을 상세히 알고 설정해야 한다.&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;acks=0&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리더 파티션으로 데이터를 전송했을 때 리더 파티션으로 데이터가 저장되었는지 확인하지 않는 다는 설정이다. 리더 파티션은 데이터가 저장된 이후에 데이터가 몇 번째 오프셋에 저장되었는지 리턴하는데, acks 가 0이면 리턴하지 않는다. 데이터가 잘 전송되었다고 가정하고 바로 다음 데이터를 전송하기 때문에 데이터 전송이 실패한 경우를 알 수 없다. retries 가 2 이상으로 설정되어 있더라도 재시도를 하지 않기 때문에 retries 옵션이 무의미하다. acks 를 0으로 했을 때의 장점은 속도는 빠르다.&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;acks=1&lt;/h4&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;acks=all or acks=-1&lt;/h4&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;acks를 all 로 설정할 경우 토픽 단위로 설정이 가능한 min.insync.replicas 옵션값에 따라 데이터 안정성이 달라진다. all 옵션값은 모든 리더, 팔로워 파티션에 저장을 뜻하는게 아니고 ISR에 포함된 파티션들을 뜻하는 것이다. min.insync.replicas 옵션은 프로듀서가 리더 파티션과 팔로워 파티션에 데이터가 저장되었는지 확인하기 위한 최소 ISR 그룹의 파티션 수이다. min.insync.replicas 옵션값이 1이라면 ISR 중 최소 1개 이상의 파티션에 데이터가 저장되었음을 확인하는 것이다. 이 경우 acks=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;min.insync.replicas 를 설정할 때는 파티션 복제 개수도 함께 고려해야 한다. 왜냐하면 운영하는 카프카 브로커 개수가 min.insync.replicas의 옵션값보다 작은 경우에는 프로듀서가 더는 데이터를 전송할 수 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;min.insync.replicas 옵션값을 3, 파티션 복제 개수를 3 으로 주었을때 한 브로커에 이상이 생기면 데이터를 전송할 수 없다. 최소한으로 복제되어야 하는 파티션 개수가 3인데 팔로워 파티션이 위치할 브로커의 개수가 부족하기 때문이다. 이때는 예외가 발생하여 토픽으로 데이터가 전송되지 않는다. 브로커가 3대인 클러스터를 운영한다면 파티션 복제 개수는 3, min.insyc.replicas를 2로 설정하고 프로듀서 acks 는 all 로 설정하는 것을 추천한다.&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;4.2.2 멱등성(idempotence) 프로듀서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멱등성 : 여러번 연산을 수행하더라도 동일한 결과를 나타내는 것을 뜻한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 카프카 프로듀서 동작 방식은 적어도 한번 전달 (at least once delivery) 을 지원한다. 적어도 한번은 전송을 하여 데이터가 유실되지 않지만, 두번 이상 전송하여 중복이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복을 막기 위해 enable.idempotence 옵션을 사용할 수 있다. 기본값은 false 지만 true 로 설정하면 데이터를 전송할때 프로듀서 PID 와 시퀀스 넘버를 함께 전달한다. 브로커는 프로듀서의 PID 와 시퀀스 넘버를 확인하여 동일한 메시지의 저장 요청이 오더라도 단 한번만 데이터를 저장함으로써 중복을 방지한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_3.png&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;227&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdGjVt/btsxqw7rzpV/afaghKH7BYu2AYvOLKZVa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdGjVt/btsxqw7rzpV/afaghKH7BYu2AYvOLKZVa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdGjVt/btsxqw7rzpV/afaghKH7BYu2AYvOLKZVa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdGjVt%2Fbtsxqw7rzpV%2FafaghKH7BYu2AYvOLKZVa0%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;679&quot; height=&quot;227&quot; data-filename=&quot;edited_3.png&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;227&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;네트워크 문제가 생겨 데이터를 다시 전송하더라도 PID 와 시퀀스 넘버를 이용해 중복을 체크해 데이터가 중복저장될 위험이 없다.&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;단 멱등성 프로듀서는 동일한 프로세스에서만 가능하다. 만약 프로듀서에 이상이 생겨 어플리케이션을 재실행한다면 PID가 달라질것이고, 동일한 메시지를 전달할때 새로운 PID 를 전달하기 때문에 브로커에서는 다른 데이터가 들어왔다고 생각해 데이터의 중복이 발생할 수 있다. 멱등성 프로듀서는 장애가 발생하지 않을 경우에만 정확히 한번 저장함을 보장한다는 것을 기억해야 한다.&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;4.2.3 트랜잭션 프로듀서&lt;/h3&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;원자성 : All or Nothing (전체 데이터를 처리하거나 전체 데이터를 처리하지 않거나)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 프로듀서를 사용하려면 enable.idempotence=true 로 설정하고 &lt;a href=&quot;http://transaction.id&quot;&gt;transaction.id&lt;/a&gt; 를 임의의 String 값으로 정의한다. 그리고 컨슈머의 isolation.level 을 read_committed로 설정하면 프로듀서와 컨슈머는 트랜잭션으로 처리 완료된 데이터만 쓰고 읽게 된다.&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;트랜잭션은 파티션의 레코드로 구분한다. 구분하기 위해서 프로듀서는 데이터를 레코드 파티션에 저장할 뿐만 아니라 트랜잭션의 시작과 끝을 표현하기 위해 트랜잭션 레코드를 한개 더 보낸다. 컨슈머는 파티션에 저장된 트랜잭션 레코드를 보고 트랜잭션이 완료(commit) 되었음을 확인하고 데이터를 가져간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.3 카프카 컨슈머&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3.1 멀티 스레드 컨슈머&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 처리량을 늘리기 위해 파티션과 컨슈머 개수를 늘려서 운영할 수 있다. 파티션을 여러 개로 운영하는 경우 데이터를 병렬처리 하기 위해 파티션 개수와 컨슈머 개수를 동일하게 맞추는게 가장 좋은 방법이다. 파티션 개수가 n개라면 컨슈머 그룹으로 묶인 컨슈머 스레드를 최대 n개 운영할 수 있다. n개의 스레드를 가진 1개의 프로세스를 운영하거나 1개의 스레드를 가진 프로세스를 n개 운영할 수 있다.&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브로커로부터 받은 레코드들을 병렬로 처리한다면 1개의 컨슈머 스레드로 받은 데이터를 빠르게 처리할 수 있다. for문으로 순차 처리하게 되면 이전 레코드 처리가 끝날 때까지 다음 레코드는 기다리게 된다. 하지만 멀티스레드를 사용하면 데이터 처리를 동시에 실행할 수 있기 때문에 처리 시간을 줄일 수 있다. Executors 를 사용하여 스레드 풀을 생성 후 poll() 메서드를 통해 받은 레코드들을 처리하는 스레드를 레코드마다 개별 실행하는 방법이 있다.&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-filename=&quot;4.png&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5zI2g/btsxp2eC2UU/qMrp818ePAKuEARXKUBsR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5zI2g/btsxp2eC2UU/qMrp818ePAKuEARXKUBsR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5zI2g/btsxp2eC2UU/qMrp818ePAKuEARXKUBsR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5zI2g%2Fbtsxp2eC2UU%2FqMrp818ePAKuEARXKUBsR1%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;673&quot; height=&quot;284&quot; data-filename=&quot;4.png&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;284&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;h4 data-ke-size=&quot;size20&quot;&gt;카프카 컨슈머 멀티 스레드 전략&lt;/h4&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;5.png&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kMsiK/btsxz0lZgWo/vI3YEY5M6aZEgLIrfgcgYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kMsiK/btsxz0lZgWo/vI3YEY5M6aZEgLIrfgcgYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kMsiK/btsxz0lZgWo/vI3YEY5M6aZEgLIrfgcgYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkMsiK%2Fbtsxz0lZgWo%2FvI3YEY5M6aZEgLIrfgcgYK%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;449&quot; height=&quot;296&quot; data-filename=&quot;5.png&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;296&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;h3 data-ke-size=&quot;size23&quot;&gt;4.3.2 컨슈머 랙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 랙(LAG)은 토픽의 최신 오프셋과 컨슈머 오프셋간의 차이를 말한다. 프로듀서가 보내는 데이터양이 컨슈머 데이터 처리량 보다 크다면 컨슈머 랙은 늘어난다. 컨슈머 랙이 0이라면 데이터 처리에 지연이 없다는 것이다.&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;blockquote data-ke-style=&quot;style3&quot;&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;컨슈머 랙은 카프카 명령어, KafkaConsumer의 metrics() 메서드, 외부 모니터링 툴을 사용하여 조회할 수 있다. 주로 외부 모니터링 툴을 사용하는데 버로우(Burrow)를 사용하여 컨슈머 랙을 조회한다. 버로우를 사용하면 컨슈머 그룹별로 랙을 확인할 수 있다. 외부 모니터링 툴을 사용하면 카프카 클러스터에 있는 모든 컨슈머, 토픽들의 랙 정보를 한번에 모니터링 할 수 있고, 이러한 모니터링 툴들은 컨슈머의 데이터 처리와는 별개로 지표를 수집하기 때문에 프로듀서나 컨슈머의 동작에 영향을 미치지 않는다는 장점이 있다.&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;/h4&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;버로우 : REST API를 통해 컨슈머랙 조회&lt;/li&gt;
&lt;li&gt;텔레그래프 : 데이터 수집 및 전달에 특화된 툴. 버로우를 조회하여 데이터를 전달할 수 있다&lt;/li&gt;
&lt;li&gt;엘라스틱서치 : 컨슈머 랙 정보를 담는 저장소 (InfluxDB 도 많이 쓰는 방법중 하나다)&lt;/li&gt;
&lt;li&gt;그라파나 : 엘라스틱서치 정보를 시각화하고 특정 조건에 따라 슬랙 알람을 보낼 수 있는 대시보드 툴&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;6.png&quot; data-origin-width=&quot;416&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/orJxz/btsxkiB5YLD/dfPBVCG0hR6K3hwIja5sk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/orJxz/btsxkiB5YLD/dfPBVCG0hR6K3hwIja5sk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/orJxz/btsxkiB5YLD/dfPBVCG0hR6K3hwIja5sk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2ForJxz%2FbtsxkiB5YLD%2FdfPBVCG0hR6K3hwIja5sk1%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;416&quot; height=&quot;387&quot; data-filename=&quot;6.png&quot; data-origin-width=&quot;416&quot; data-origin-height=&quot;387&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;h3 data-ke-size=&quot;size23&quot;&gt;4.3.3 컨슈머 배포 프로세스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 시간 동안 중단이 되어 지연이 발생하더라도 서비스 운영에 지장이 없다면 중단 배포를 사용하고, 중단이 발생했을때 서비스 영향이 클 경우에는 무중단 배포를 수행해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;중단배포&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중단 배포는 기존 어플리케이션을 완전히 중단하고 신규 어플리케이션을 실행한다. 중단배포를 실행할 경우 새로운 로직이 적용된 신규 어플리케이션의 실행 전후를 명확하게 특정 오프셋 지점으로 나눌 수 있다는 점이 장점이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;무중단배포&lt;/h4&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전 버전 어플리케이션과 신규 어플리케이션을 동시에 띄어놓고 트래픽을 전환하는 방법&lt;/li&gt;
&lt;li&gt;파티션 개수와 컨슈머 개수가 동일할 때 유용하다&lt;/li&gt;
&lt;li&gt;동일하지 않다면 일부 파티션은 기존 어플리케이션에 할당되고 일부 파티션은 신규 어플리케이션에 할당되어 섞이기 때문에 적합하지 않은 방법이다.&lt;/li&gt;
&lt;li&gt;신규 버전이 준비되면 기존 어플리케이션을 중단한다. 그러면 리밸런싱이 발생하면서 파티션은 모두 신규 컨슈머와 연동된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;롤링
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파티션 개수가 인스턴스 개수와 같거나 그보다 많아야 한다&lt;/li&gt;
&lt;li&gt;리밸런스가 여러번 발생한다&lt;/li&gt;
&lt;li&gt;컨슈머 스레드 하나씩 신규버전으로 교체하는 방법이라고 생각하면 된다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;카나리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신규 버전 문제가 있는지 사전에 탐지할 수 있다.&lt;/li&gt;
&lt;li&gt;소수 파티션에 신규 버전 어플리케이션을 할당하면서 사전 테스트를 하는 방법&lt;/li&gt;
&lt;li&gt;카나리 배포로 사전 테스트가 완료되면 나머지 파티션에 대해 롤링 or 블루/그린 배포를 수행하면 무중반 배포가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.4 스프링 카프카&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 카프카에서는 KafkaTemplate 을 통해 데이터를 전송할 수 있다. KafkaTemplate 은 스프링 카프카에서 제공하는 기본 클래스를 사용하는 방법과 ProducerFactory 로 생성하는 방법이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4.1 스프링 카프카 프로듀서&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본 카프카 템플릿&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yaml 에 옵션에 입력하고 사용하면 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;커스텀 카프카 템플릿&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서 팩토리를 통해 만든 카프카 템플릿 객체를 빈으로 등록하여 사용하는 것이다. 스프링 카프카 어플리케이션 내부에 다양한 종류의 카프카 프로듀서 인스턴스를 생성하고 싶다면 이 방식을 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;private Map&amp;lt;String, Object&amp;gt; producerConfig() {
    Map&amp;lt;String, Object&amp;gt; configProps = new HashMap&amp;lt;&amp;gt;();
    configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
    configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    configProps.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 10000);
    configProps.put(ProducerConfig.ACKS_CONFIG, &quot;all&quot;);
    configProps.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 1000);
    configProps.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 100 * 1000); // DEFAULT 120초. send() 후 성공or실패를 보고하는 시간의 상한
    configProps.put(ProducerConfig.LINGER_MS_CONFIG, 0); // DEFAULT 0 초. Batch 처리 될때까지 대기하는 시간. 0초로 하면 즉시전송.
    return configProps;
}
private ProducerFactory&amp;lt;String, Object&amp;gt; producerFactory() {
    return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(producerConfig());
}
@Bean
public KafkaTemplate&amp;lt;String, Object&amp;gt; kafkaTemplate() {
    return new KafkaTemplate&amp;lt;&amp;gt;(producerFactory());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 KafkaTemplate 빈을 생성하고 사용하면 됩니다. 아래 예시는 ListenableFutureCallback을 사용해 메시지 전송에 성공, 실패했을때 실행되는 콜백 메서드를 구현했습니다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;String message = objectMapper.writeValueAsString(payMessage);
ListenableFuture&amp;lt;SendResult&amp;lt;String, Object&amp;gt;&amp;gt; future = kafkaTemplate.send(&quot;pay-test-topic&quot;,&quot;test-key&quot;, message);
future.addCallback(new ListenableFutureCallback&amp;lt;SendResult&amp;lt;String, Object&amp;gt;&amp;gt;() {
    @Override
    public void onFailure(Throwable ex) {
        ex.printStackTrace();
        log.error(&quot;Message Exception:{}&quot;, ex);
    }

    @Override
    public void onSuccess(SendResult&amp;lt;String, Object&amp;gt; result) {
        ProducerRecord&amp;lt;String, Object&amp;gt; producerRecord = result.getProducerRecord();
        RecordMetadata recordMetadata = result.getRecordMetadata();
        log.info(&quot;topic: {},partition: {}, offset: {}&quot;,
                recordMetadata.topic(), recordMetadata.partition(), recordMetadata.offset());
        log.info(&quot;topic: {}, header: {}, key: {}, partition: {}&quot;,
                producerRecord.topic(), producerRecord.headers(), producerRecord.key(), producerRecord.partition());
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4.2 스프링 카프카 컨슈머&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 카프카 컨슈머는 레코드 리스너(MessageListener)와 배치 리스너(BatchMessageListener)가 있다. 리스너 종류에 따라 한번 호출하는 메서드에서 처리하는 레코드의 개수가 달라진다. 레코드 리스너는 1개를 처리하고, 배치 리스너는 poll() 메서드로 리턴받은 ConsumerRecords 처럼 한번에 여러개 레코드들을 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 설정을 가진 2개 이상의 리스너를 구현하거나 리밸런스 리스너를 구현하기 위해선 커스텀 리스너 컨테이너를 구현해야 한다. 카프카 리스너 컨테이너 팩토리를 빈으로 등록하고 KafkaListener 어노테이션에서 커스텀 리스너 컨테이너 팩토리를 등록하면 커스텀 리스너 컨테이너를 사용할 수 있다. 아래는 커스텀 리스너 컨테이너를 만드는 예시다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;/*
MAX_POLL_RECORDS_CONFIG : poll() 을 호출해 Topic 에서 한번에 가져오는 메시지 개수
MAX_POLL_INTERVAL_MS_CONFIG : 컨슈머가 poll 하고 commit 하기까지의 최대 시간
HEARTBEAT_INTERVAL_MS_CONFIG : 컨슈머가 heartbeat 를 보내는 주기. sessionTimeout 보다 작아야하고 일반적으로 1/3으로 설정.
SESSION_TIMEOUT_MS_CONFIG : 컨슈머와 브로커 사이의 세션 유지 시간
REQUEST_TIMEOUT_MS_CONFIG : 요청의 응답을 기다리는 최대 시간
 */

private Map&amp;lt;String, Object&amp;gt; consumerConfig() {
    Map&amp;lt;String, Object&amp;gt; configMap = new HashMap&amp;lt;&amp;gt;();
    configMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
    configMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
    configMap.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, &quot;1&quot;);
    configMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    configMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    return configMap;
}

private ConsumerFactory&amp;lt;Object, Object&amp;gt; consumerFactory() {
    return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(consumerConfig());
}

@Bean
public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory(
        @Qualifier(&quot;deadLetterPublishingRecoverer&quot;) DeadLetterPublishingRecoverer deadLetterPublishingRecoverer) {
    ConcurrentKafkaListenerContainerFactory&amp;lt;Object, Object&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
    factory.setConsumerFactory(consumerFactory());
    factory.setConcurrency(1);
    factory.setBatchListener(false);
    factory.setCommonErrorHandler(errorHandler(deadLetterPublishingRecoverer));
    factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.RECORD);
    return factory;
}

private DefaultErrorHandler errorHandler(DeadLetterPublishingRecoverer deadLetterPublishingRecoverer) {
    BackOff fixedBackOff = new FixedBackOff(1000L, 3L);
    DefaultErrorHandler errorHandler = new DefaultErrorHandler(deadLetterPublishingRecoverer, fixedBackOff);
    errorHandler.addRetryableExceptions(SocketTimeoutException.class, MessageRetryHandleException.class);
    errorHandler.addNotRetryableExceptions(NullPointerException.class, NotCreateTopicMessageException.class, ValidationException.class);
    return errorHandler;
}
&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;위 예에서 보이는 ErrorHandler, DeadLetterPublishingRecoverer 을 사용한 로직은 메시지를 처리하다가 발생한 예외를 처리하는 로직이다. 재시도 정책과 관련된 간단한 구현 예이니 참고하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Configuration
public class MemberUpdateDeadLetterPublishingRecovererConfig {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Bean
    public DeadLetterPublishingRecoverer deadLetterPublishingRecoverer(KafkaTemplate&amp;lt;String, String&amp;gt; template) {
        return new DeadLetterPublishingRecoverer(template,
                (consumerRecord, ex) -&amp;gt; {
                    logger.error(&quot;=== consumerRecord: {}&quot;, consumerRecord);
                    return new TopicPartition(&quot;DLT토픽이름&quot;, consumerRecord.partition());
                });
    }
}
&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;pre class=&quot;lasso&quot;&gt;&lt;code&gt;@KafkaListener(
        topics = &quot;토픽이름&quot;,
        groupId = &quot;그룹아이디&quot;,
        containerFactory = &quot;kafkaListenerContainerFactory&quot;
)
public void onMessage(ConsumerRecord&amp;lt;String, String&amp;gt; data) {
    logger.info(&quot;offset: {}, message: {}&quot;, data.offset(), message);
	// process...
}
&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;위 예는 AckMode 를 RECORD 로 했을 때의 예시이다. 레코드 단위로 처리후 커밋을 하는 방식이다. 하지만 Acknowledgement.acknowledge() 메서드 호출을 통해 커밋 하는 타이밍을 직접 제어하고 싶으면 AckMode 를 MANUAL_IMMEDIATE 로 변경하고 아래와 같은 리스너를 구현하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// AcknowledgingMessageListener 구현
// onMessage 파라미터에 Acknowledgement 추가
@Component
public class TopicListener implements AcknowledgingMessageListener&amp;lt;String, String&amp;gt; {
		
		@KafkaListener(
		    topics = &quot;토픽이름&quot;,
		    groupId = &quot;그룹아이디&quot;,
		    containerFactory = &quot;kafkaListenerContainerFactory&quot;
		)
		public void onMessage(ConsumerRecord&amp;lt;String, String&amp;gt; data, Acknowledgment ack) {
				logger.info(&quot;offset: {}, message: {}&quot;, data.offset(), message);
				// process...
				ack.acknowledge();
		}

}
&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;동기 커밋, 비동기 커밋을 사용하고 싶다면 consumer 인스턴스를 onMessage 파라미터로 추가해서 사용할 수 있다. consumer 인스턴스의 commitSync(), commitAsync() 메서드를 호출하면 사용자가 원하는 타이밍에 커밋할 수 있도록 로직을 추가할 수 있다. 다만, 리스너가 커밋하지 않도록 AckMode 는 MANUAL, MANUAL_IMMEDIATE 로 설정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>Kafka</category>
      <category>Topic</category>
      <author>Jadie Blog</author>
      <guid isPermaLink="true">https://yongdev.tistory.com/160</guid>
      <comments>https://yongdev.tistory.com/160#entry160comment</comments>
      <pubDate>Sun, 8 Oct 2023 13:54:40 +0900</pubDate>
    </item>
    <item>
      <title>The Go Programming 5</title>
      <link>https://yongdev.tistory.com/158</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;The Go Programming Language - 에이콘출판사 책을 정리한 글 입니다.&lt;/b&gt;&lt;/p&gt;
&lt;h1&gt;The Go Programming&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Chapter 5. 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;함수는 여러 문장을 하나의 단위로 묶어 프로그램 내의 다른 부분에서 수차례 호출 할 수 있다. 함수를 통해 큰 작업을 여러 작은 작업으로 분할할 수 있으며, 함수를 사용하면 사용자에게 구현의 세부 사항을 숨길 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 함수 선언&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수는 아래와 같은 형식으로 이루어져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;func 이름(파라미터 목록) (결과 목록) { 본문 }&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파라미터 목록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수 파라미터의 이름과 타입을 지정&lt;/li&gt;
&lt;li&gt;이 인자는 함수를 호출하는 호출자가 값이나 인자를 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결과 목록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수가 반환하는 값의 타입을 지정한다.&lt;/li&gt;
&lt;li&gt;함수가 한개의 이름없는 결과를 반환하거나 결과를 반환하지 않을 경우엔 괄호를 사용할 필요가 없다.&lt;/li&gt;
&lt;li&gt;결과목록을 생략하면 아무 값도 반환하지 않으며, 그 동작을 위해서만 호출되는 함수를 선언한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1611489827440&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main import( &quot;fmt&quot; &quot;math&quot; )
func hypot(x, y float64) float64 {
	return math.Sqrt(x*x + y*y)
}

func main() {
	fmt.Println(hypot(3, 4)) // 결과는 5
}&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;x와 y는 파라미터이고 3과 4는 호출자가 제공하는 인수이며, 이 함수는 float 64 값을 반환한다. 그리고 파라미터와 마찬가지로 결과에도 이름을 붙일 수 있다. 아래 함수는 위의 hypot 함수의 결과에 이름을 붙인 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1611489869800&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func hypot(x, y float64) (r float64) {
	r = math.Sqrt(x*x + y*y)
    return r
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;함수의 인자는 값으로 전달되므로 함수는 각 인자의 복사본을 전달 받는다. 이 복사본에 대한 변경은 호출자에 아무 영향을 주지 않지만, 만약 인자가 포인터, 슬라이스, 맵, 함수, 채널 등의 참조형인 경우에는 값의 변경이 호출자에 영향을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 재귀&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;함수는 스스로를 직접 또는 간접적으로 재귀 호출할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(재귀함수에 대해선 따로 알아보자. 여기선 예시 코드를 보고 넘어가자)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;fetch.go&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1611489950392&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main import ( &quot;fmt&quot; &quot;io/ioutil&quot; &quot;net/http&quot; &quot;os&quot; )
func main() {
	for _, url := range os.Args[1:] {
    	resp, err := http.Get(url)
        if err != nil { 
        	fmt.Fprintf(os.Stderr, &quot;fetch: %v\\n&quot;, err) os.Exit(1) 
         }
     	b, err := ioutil.ReadAll(resp.Body)
     	resp.Body.Close()
     	if err != nil {
     		fmt.Fprintf(os.Stderr, &quot;fetch: reading %s: %v\\n&quot;, url, err) 
        	os.Exit(1) 
     	}
     	fmt.Printf(&quot;%s&quot;, b)
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;outline.go&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1611490100955&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main
import (
	&quot;fmt&quot;
	&quot;golang.org/x/net/html&quot;
	&quot;os&quot;
)
func main() {
	doc, err := html.Parse(os.Stdin)
    // zero value: 초기값을 할당하지 않고 변수를 만들었을 때 해당 변수가 갖는 값
    // nil은 포인터, 인터페이스, 맵, 슬라이스, 채널, 함수의 zero value다.
    if err != nil {
    	fmt.Fprintf(os.Stderr, &quot;outline: %v\\n&quot;, err)
        os.Exit(1) 
     }
     outline(nil, doc)
}
func outline(stack []string, n *html.Node) {
	if n.Type == html.ElementNode {
    	stack = append(stack, n.Data) 
        fmt.Println(stack)
	}
    for c := n.FirstChild; c != nil; c = c.NextSibling {
    	outline(stack, c)
    }
}

&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;letter-spacing: 0px;&quot;&gt;&amp;nbsp;outline.go 프로그램은 HTML 노드 트리에 재귀를 사용해 개략적인 트리 구조를 출력한다. 재귀실행 하면서 마주치는 각각의 요소의 태그를 스택에 넣고 스택을 출력한다. (fetch.go 는 지정된 URL 에서 HTML 정보를 가져오기 위한 코드인데, 해당 재귀 프로그램을 설명하기 위한 도우미 코드다.)&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;&amp;nbsp;아래 명령어는 실행 코드 와 결과를 보여준다. go는 build 명령어를 통해 바이너리 파일을 생성할 수 있다. 아래 명령어는 fetch.go 를 빌드해서 생성된 fetch 바이너리 파일의 실행 결과를 바이너리 파일 outline 의 입력으로 사용하는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ go build fetch.go $ go build outline.go&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ ./fetch &lt;a href=&quot;https://golang.org&quot;&gt;https://golang.org&lt;/a&gt;&amp;nbsp;| ./outline2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html head]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html head meta]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html head meta]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html head title]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html head link]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html head link]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html head script]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html head script]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html body]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html body header]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html body header div]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html body header div a]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html body header nav]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html body header nav a]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html body header nav a img]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[html body header nav button]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;...&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;재귀호출을 통해 HTML 문서의 태그만 출력한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 다중 값 반환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;함수는 결과를 한 개 이상 반환할 수 있다. 다음 findLinks 함수는 링크 목록과 오류, 2개의 값을 반환하는 함수다.&lt;/p&gt;
&lt;pre id=&quot;code_1611490635407&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main 
import (
  &quot;fmt&quot;
  &quot;net/http&quot;
  &quot;golang.org/x/net/html&quot;
  &quot;os&quot;
)

func main() {
	for _, url := range os.Args[1:] {
    	links, err := findLinks(url)
        if err != nil { 
        	fmt.Fprintf(os.Stderr, &quot;findlinks2: %v\\n&quot;, err)
		continue
        }
        for _, link := range links {
        	fmt.Println(link)
        }
    }
}

// findLinks 는 url 에 있는 get 요청을 수행하고
// 결과를 html로 파싱한 후 링크를 추출하고 반환한다.

func findLinks(url string) ([]string, error) {
	resp, err := http.Get(url)
	if err != nil {
    	return nil, err
    }
	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, fmt.Errorf(&quot;getting %s: %s&quot;, url, resp.Status)
    }
	doc, err := html.Parse(resp.Body)
	resp.Body.Close()
	if err != nil {
		return nil, fmt.Errorf(&quot;parsing %s as HTML: %v&quot;, url, err)
	}
	return visit(nil, doc), nil
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;다중 값을 반환하는 함수를 호출할 때는 변수에 명시적으로 값을 할당해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1611490756671&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;links, err := findLinks(url)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환 값중 일부를 무시하려면 값을 빈 식별자에 할당하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1611490764206&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;links, _ := findLinks(url)&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_1611490786850&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func CountWordsAndImages(url string) (words, images int, err error) {
	...
	return
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;단순 반환은 이름이 있는 변수를 순서대로 반환하는 단축 문법이다. 하지만 코드를 이해하기 쉽게 하는 경우는 거의 없다. 코드 수를 줄일 수 있다는 장점에 비해 단점이 너무 크기에 삼가는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.4 오류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;오류 처리 전략의 가장 흔한 방법으로는 오류를 전파해 서부루틴에서의 실패를 호출 루틴의 실패가 되게 하는 전략이 있다. 아래 코드는 http 오류를 호출자에게 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1611490862978&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resp, err := http.Get(url)
if err != nil {
	return nil, err
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;두번째 전략은 지연시간을 두거나 재시도 횟수 또는 재시도 소요시간을 제한하고 실패한 작업을 다시 시도하는 전략이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1611490961701&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// url 로 지정된 서버로 접속을 시도
// 1분간 지수 단위로 백오프 수행
// 모든 시도가 실패하면 오류를 보고
func waitForserver(url string) error {
	const timeout = 1 * time.Minute
	deadline := time.Now().Add(timeout)
	for tries := 0; time.Now().Before(deadline); tries++ {
		_, err := http.Head(url)
		if err == nil {
			return nil
		}
		log.Printf(&quot;server not responding (%s); retrying...&quot;, err)
		time.Sleep(time.Second &amp;lt;&amp;lt; uint(tries)) // 지수 단위 백오프
	}
	return fmt.Errorf(&quot;server %s failed to respond after %s&quot;, url, timeout)
}&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;letter-spacing: 0px;&quot;&gt;&amp;nbsp;세번째 전략은 호출자가 오류를 출력하고 프로그램을 종료하는 방법이 있다. 이러한 역할은 보통 main 패키지의 역할이다.&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;&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;Go 의 오류처리에는 특유의 리듬이 있다. 오류를 확인한 후 실패를 성공 전에 처리한다. 실패로 인한 함수가 반환돼야 한다면 성공에 대한 로직은 들여쓰기 한 else 블록이 아닌 외부 수준에 작성을 한다.&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;5.5 함수 값&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Go의 함수는 first-class(일급) 값이다. 이것은 함수 값이 다른 값과 마찬가지로 타입이 있고 함수값을 변수에 할당이 가능, 함수값을 함수 파라미터로 전달이 가능, 함수에서 반환이 가능하다는 것이다. 함수값은 임의의 다른 함수처럼 호출할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1611491067334&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;strings&quot;
)

func square(n int) int { return n * n }
func main() {
	f1 := square
	fmt.Println(f1(3)) // 9 출력

	var f func(int) int // 함수 타입의 제로 값은 nil 이다.
	fmt.Println(f(3))   // panic: runtime error: invalid memory address or nil pointer dereference
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;함수 값을 이용해 함수 값뿐만 아니라 동작도 전달할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1611491207573&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func add2(r rune) rune {
	return r + 1
}
func main() {
	fmt.Println(strings.Map(add2, &quot;VMS&quot;)) // WNT 출력
}&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;5.2 에서 visit 을 사용해서 HTML의 모든 노드를 방문해 출력하는 코드가 있었는데, 이번에는 다르게 만들어보자. 이번엔 함수 값을 사용해 트리 탐색을 위한 로직과 각 노드에 적용할 액션 로직을 분리해서 만들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1611491307258&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;golang.org/x/net/html&quot;
	&quot;net/http&quot;
	&quot;os&quot;
)

func main() {
	for _, url := range os.Args[1:] {
		outline2(url)
	}
}

func outline2(url string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	doc, err := html.Parse(resp.Body)
	if err != nil {
		return err
	}

	forEachNode(doc, startElement, endElement)

	return nil
}

func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
	if pre != nil {
		pre(n)
	}
	for c := n.FirstChild; c != nil; c = c.NextSibling {
		forEachNode(c, pre, post)
	}
	if post != nil {
		post(n)
	}
}

var depth int

func startElement(n *html.Node) {
	if n.Type == html.ElementNode {
		fmt.Printf(&quot;%*s&amp;lt;%s&amp;gt;\n&quot;, depth*2, &quot;&quot;, n.Data)
		depth++
	}
}

func endElement(n *html.Node) {
	if n.Type == html.ElementNode {
		depth--
		fmt.Printf(&quot;%*s&amp;lt;/%s&amp;gt;\n&quot;, depth*2, &quot;&quot;, n.Data)
	}
}
&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;forEachNode 함수는 하위 노드를 방문하기 전에 호출할 함수와 방문한 후에 호출함 함수 두개를 각각 인자로 받는다. startElement, endElement 에서 %s 의 * 포매터는 앞에 가변 길이의 공백이 추가된 문자열을 출력한다. 공백의 폭과 문자열로 depth2와 &quot;&quot; 인자를 사용했다.&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;출력 결과는 아래와 같다 (defer 명령어에 대해선 아래 연기된 함수에 나와있다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ go build outline2.go&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ ./outline2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;html&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;head&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;meta&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;/meta&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;title&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;/title&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;script&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;link&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;/link&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;style&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;/style&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;/head&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;body&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;table&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;lt;tbody&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;lt;tr&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;lt;td&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;a&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;img&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;/img&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;...&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.44em; letter-spacing: -1px;&quot;&gt;5.6 익명함수&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;&amp;nbsp;명명된 함수는 패키지 수준에서만 선언할 수 있지만, 함수 리터럴로 표현식 내의 어디서나 함수 값을 나타낼 수 있다. 함수 리터럴은 함수 선언과 유사하게 작성하지만 func 키워드 뒤에 이름이 없다. 이는 표현식이며, 그 값을 익명 함수라 한다. 예를 들면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1611491477590&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;strings.Map(func(r rune) rune {return r+1}, &quot;VMS&quot;} // WNT 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;익명함수는 내부 함수에서 외부 변수를 참조할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1611491503255&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func squares() func() int {
	var x int
	return func() int {
		x++
		return x * x
	}
}

func main() {
	f := squares()
	fmt.Println(f()) // 1
	fmt.Println(f()) // 4
	fmt.Println(f()) // 9
	fmt.Println(f()) // 16
}
&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;squares 함수는 func() int 타입의 다른 함수를 반환한다. 익명 내부 함수는 바깥에 있는 square 함수의 지역 변수에 접근하고 값을 갱신할 수 있다. 이러한 숨겨진 변수 참조가 있기 때문에 함수는 참조 타입으로 정의한다. 함수 값은 closure 라는 기술로 구현하며, go 개발자들은 보통 함수 값이라는 용어를 사용한다.&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;5.7 가변 인자 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;가변 인자 함수는 다양한 개수의 인자로 호출할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1611491523063&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func sum(vals ...int) int {
	total := 0
	for _, val := range vals {
		total += val
	}
	return total
}

func main() {
	fmt.Println(sum())           // 0
	fmt.Println(sum(3))          // 3
	fmt.Println(sum(1, 2, 3, 4)) // 10
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;sum 함수는 0개 이상의 int 인자를 받아서 합을 반환한다. 함수 본문 안에서 vals의 타입은 []int 슬라이스다. 함수를 호출하는 호출자는 묵시적으로 배열을 할당하고 배열에 인자를 복사한 후 함수에 전체 배열의 슬라이스를 전달한다.&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;5.8 연기된 함수 호출&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;다음 프로그램은 HTML 문서를 가져와 제목을 출력한다. title 함수는 서버 응답에서 Content-Type 헤더를 검사하고 문서가 HTML이 아닐 경우 오류를 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1611491700641&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;net/http&quot;
	&quot;os&quot;
	&quot;strings&quot;

	&quot;golang.org/x/net/html&quot;
)

func main() {
	for _, arg := range os.Args[1:] {
		if err := title(arg); err != nil {
			fmt.Fprintf(os.Stderr, &quot;title: %v\n&quot;, err)
		}
	}
}


func title(url string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}

	// Check Content-Type is HTML (e.g., &quot;text/html; charset=utf-8&quot;).
	ct := resp.Header.Get(&quot;Content-Type&quot;)
	if ct != &quot;text/html&quot; &amp;amp;&amp;amp; !strings.HasPrefix(ct, &quot;text/html;&quot;) {
		resp.Body.Close()
		return fmt.Errorf(&quot;%s has type %s, not text/html&quot;, url, ct)
	}

	doc, err := html.Parse(resp.Body)
	resp.Body.Close()
	if err != nil {
		return fmt.Errorf(&quot;parsing %s as HTML: %v&quot;, url, err)
	}

	visitNode := func(n *html.Node) {
		if n.Type == html.ElementNode &amp;amp;&amp;amp; n.Data == &quot;title&quot; &amp;amp;&amp;amp;
			n.FirstChild != nil {
			fmt.Println(n.FirstChild.Data)
		}
	}
	forEachNode(doc, visitNode, nil)
	return nil
}

func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
	if pre != nil {
		pre(n)
	}
	for c := n.FirstChild; c != nil; c = c.NextSibling {
		forEachNode(c, pre, post)
	}
	if post != nil {
		post(n)
	}
}&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;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;$ go build title.go&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;$ ./title &lt;/span&gt;&lt;a href=&quot;http://gopl.go&quot;&gt;http://gopl.go&lt;/a&gt;&lt;span style=&quot;color: #333333;&quot;&gt; &amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;The Go Programming Language&lt;/span&gt;&lt;/p&gt;
&lt;/div&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; title 에 네트워크 접속을 닫게 하는 resp.Body.Close() 호출이 중복돼 있다. 함수가 커지면서 복잡해지고 많은 오류를 처리해야 할 일이 생길 때는 이런 중복된 정리 로직이 유지보수에 문제가 될 가능성이 있다. go 의 defer 를 사용하면 이런 문제를 해결할 수 있다.&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; defer 키워드가 붙은 함수는 정상적으로 반환문 또는 끝에 도달하거나 비정상적인 패닉이 일어나서 완료될 때까지 미뤄진다. defer문은 보통 open과 close, connect와 disconnect, lock과 unlock 과 같이 한 쌍의 작업을 해야할 때 함수의 복잡도와 관계없이 확실하게 리소스를 해제하기 위해 자주 사용된다. 리소스를 해제하는 defer 문의 적절한 위치는 리소스가 성공적으로 할당된 직후다. 위의 title 함수에 defer 문을 사용하면 아래와 같은 코드가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1611491824531&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func title2(url string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	ct := resp.Header.Get(&quot;Content-Type&quot;)
	if ct != &quot;text/html&quot; &amp;amp;&amp;amp; !strings.HasPrefix(ct, &quot;text/html;&quot;) {
		return fmt.Errorf(&quot;%s has type %s, not text/html&quot;, url, ct)
	}

	doc, err := html.Parse(resp.Body)
	if err != nil {
		return fmt.Errorf(&quot;parsing %s as HTML: %v&quot;, url, err)
	}

	// title 엘리먼트 출력
	visitNode := func(n *html.Node) {
		if n.Type == html.ElementNode &amp;amp;&amp;amp; n.Data == &quot;title&quot; &amp;amp;&amp;amp;
			n.FirstChild != nil {
			fmt.Println(n.FirstChild.Data)
		}
	}
	forEachNode(doc, visitNode, nil)

	return nil
}&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;defer 문은 디버깅할 때 사용하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1611491885949&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main

import (
	&quot;log&quot;
	&quot;time&quot;
)

func bigSlowOperation() {
	defer trace(&quot;bigSlowOperation&quot;)()
	time.Sleep(10 * time.Second)
}

func trace(msg string) func() {
	start := time.Now()
	log.Println(&quot;enter %s&quot;, msg)
	return func() { log.Printf(&quot;exit %s (%s)&quot;, msg, time.Since(start)) }
}

func main() {
	bigSlowOperation()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;결과는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1611491913183&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2021/01/24 02:45:35 enter %s bigSlowOperation
2021/01/24 02:45:45 exit bigSlowOperation (10.001815647s)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;bigSlowOperation 함수는 trace 를 호출해 시작 시간에 대한 액션을 수행하고, 이후에 종료 시간 액션을 수행하는 함수 값을 반환한다. 이와 같이 반환되는 함수에 대한 호출을 연기해 함수의 시작 부분과 모든 종료부분을 하나의 문장으로 조작할 수 있다.&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;5.9 패닉&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Go는 컴파일시 많은 실수를 잡아내지만 배열 범위 바깥쪽 참조나 nil 포인터 참조 등의 실행 시 검사가 필요한 경우도 있다. Go 런타임이 이러한 실수를 발견하면 패닉을 발생시킨다.&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;보통 패닉 상황에서는 정상 실행이 중단되고 고루틴에 있는 모든 연기된 함수가 호출되며, 프로그램 로그 메시지와 함께 비정상 종료된다. 이 로그 메시지에는 패닉값이 들어 있으며, 프로그램을 다시 실행하지 않고도 문제 원인을 파악할 수 있을 만큼의 충분한 정보가 있으므로 패닉을 일으키는 프로그램에 관한 버그 리포트에는 항상 포함되어야 한다. 패닉은 일부 '불가능한 상황', 이를테면 실행 중 논리적으로 일어날 수 없는 경우에 가장 좋은 대응 방법이다. 아래 코드는 card 가 예상된 종류의 카드가 아닐시 패닉을 일으키게 하는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1611491959950&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;switch s := suit(drawCard()); s {
	case &quot;Spades&quot;: //
	case &quot;Hearts&quot;: //
	case &quot;Diamonds&quot;: //
	case &quot;Clubs&quot;: //
	default:
		panic(fmt.Sprintf(&quot;invalid suit %q&quot;, s)) // Joker?
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;패닉이 발생하면 모든 연기된 함수가 스택 맨 위에서 main 까지 역순으로 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1611492030928&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
	f(3)
}

func f(x int) {
	fmt.Printf(&quot;f(%d) \n&quot;, x+0/x)
	defer fmt.Printf(&quot;defer %d\n&quot;, x)
	f(x - 1)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 함수의 실행결과는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;f(3)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;f(2)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;f(1)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;defer 1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;defer 2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;defer 3&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;panic: runtime error: integer divide by zero&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;goroutine 1 [running]:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.f(0x0) /MyWorkspace/go-programming/src/chapter5/defer1.go:10 +0x1e5&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.f(0x1) /MyWorkspace/go-programming/src/chapter5/defer1.go:12 +0x185&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.f(0x2) /MyWorkspace/go-programming/src/chapter5/defer1.go:12 +0x185&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.f(0x3) /MyWorkspace/go-programming/src/chapter5/defer1.go:12 +0x185&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.main() /MyWorkspace/go-programming/src/chapter5/defer1.go:6 +0x2a&lt;/p&gt;
&lt;/div&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;f(0) 호출에서 패닉이 발생해 세 개의 연기된 fmt.Printf 호출이 수행된다.&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;5.10 복구&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;내장된 recover 함수는 연기된 함수(defer) 안에서 호출되며, defer 구문이 들어있는 함수가 패닉을 일으키면 recover 함수가 현재의 패닉 상태를 끝내고 패닉 값을 반환한다. 패닉을 일으킨 함수는 마지막 부분을 계속하지 않고 정상적으로 반환한다. 패닉이 아닐때 recover 를 호출하면 nil 을 반환한다.&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;복구는 신중하게 해야 한다. 네트워크 접속이 열린 이후 닫히지 않았거나, 락을 걸고 나서 해제하지 않게 되는 문제가 발생할 수도 있다. 또는 비정상 종료를 로그 파일의 한 줄로 교체하는 식의 무차별 복구로 인해 버그를 알아차리지 못하게 되는 경우도 있다. 복구는 일반적으로 '복구될 의도가 있는 드문 경우'에만 복구해야 한다. 복구 대상이면 패닉을 일반 error 로 보고하고, 복구 대상이 아니라면 패닉 상태에서 재개하기 위해 같은 값으로 panic 을 호출한다. 다음 예제는 HTML 문서에 &amp;lt;title&amp;gt; 원소가 여러개 있는 경우 오류를 보고한다. 이 경우 특별한 타입 값 ballout 으로 panic 을 호출해 재귀를 중단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1611492196641&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func soleTitle(doc *html.Node) (title string, err error) {
	type bailout struct{}

	defer func() {
		switch p := recover(); p {
		case nil:
			// 패닉이 아님
		case bailout{}:
			// 예상된 패닉
			err = fmt.Errorf(&quot;multiple title elements&quot;)
		default:
			panic(p) // 예상치 못한 패닉, 패닉 지속 
		}
	}()

     // 비어있지 않은 title이 두개 이상이면 재귀에서 탈출.
	forEachNode(doc, func(n *html.Node) {
		if n.Type == html.ElementNode &amp;amp;&amp;amp; n.Data == &quot;title&quot; &amp;amp;&amp;amp;
			n.FirstChild != nil {
			if title != &quot;&quot; {
				panic(bailout{}) // multiple title elements
			}
			title = n.FirstChild.Data
		}
	}, nil)
	if title == &quot;&quot; {
		return &quot;&quot;, fmt.Errorf(&quot;no title element&quot;)
	}
	return title, nil
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;지연된 함수가 recover 를 호출해 패닉 값을 확인하고 그 값이 ballout{} 이라면 일반 오류로 보고한다. 그 외의 nil이 아닌 모든 값은 예상치 못한 패닉이므로 핸들러는 그 값으로 panic 을 호출해 recover를 취소하고 패닉의 원래 상태를 재개한다. 복구할 수 없는 경우도 있다. 예를 들어 메모리가 부족하면 Go 런타임이 치명적인 오류와 함께 프로그램을 종료하게 된다.&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;이상으로 5장 함수를 마칩니다.&lt;/p&gt;</description>
      <category>ETC</category>
      <category>go</category>
      <author>Jadie Blog</author>
      <guid isPermaLink="true">https://yongdev.tistory.com/158</guid>
      <comments>https://yongdev.tistory.com/158#entry158comment</comments>
      <pubDate>Sun, 24 Jan 2021 21:12:49 +0900</pubDate>
    </item>
  </channel>
</rss>