<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>깻묵의 이야기</title>
    <link>https://netal.tistory.com/</link>
    <description>가천대학교 소프트웨어학과에 재학중인 천진우입니다.
좋은 글을 주기적으로 작성하기 위해 노력합니다.</description>
    <language>ko</language>
    <pubDate>Mon, 24 Aug 2026 21:02:56 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>깻묵(천진우)</managingEditor>
    <image>
      <title>깻묵의 이야기</title>
      <url>https://tistory1.daumcdn.net/tistory/2675030/attach/293319345d69489abd679f11ff85f615</url>
      <link>https://netal.tistory.com</link>
    </image>
    <item>
      <title>[Spring] WebClient failed: Connection reset by peer; 해결해보기</title>
      <link>https://netal.tistory.com/entry/Spring-WebClient-failed-Connection-reset-by-peer-%ED%95%B4%EA%B2%B0%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
      <description>&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;전 회사에서도, 이번 회사에서도 webclient 를 쓰는 코드가 존재하며, 명확하게 알지 못하고 사용하여 문제가 발생했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713074959401&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.springframework.web.reactive.function.client.WebClientRequestException:
readAddress(..) failed: Connection reset by peer;&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;connection reset by peer 면, 상대방이 연결을 끊었다는 뜻이 아닌가 ... ?&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;그러면 LB 쪽에서 끊는건가? &quot;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://repost.aws/ko/articles/ARIFCVXv6-QV2HIvnJ4px1Zw/network-load-balancer-nlb-troubleshooting-guide&quot;&gt;https://repost.aws/ko/articles/ARIFCVXv6-QV2HIvnJ4px1Zw/network-load-balancer-nlb-troubleshooting-guide&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713075107502&quot; style=&quot;color: #333333; text-align: start;&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Uya8X/hyUXQ8iApR/t9tKIvtn3T3k2kBYGsf9tk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot; data-og-url=&quot;https://repost.aws/articles/ARIFCVXv6-QV2HIvnJ4px1Zw/network-load-balancer-nlb-troubleshooting-guide&quot; data-og-source-url=&quot;https://repost.aws/ko/articles/ARIFCVXv6-QV2HIvnJ4px1Zw/network-load-balancer-nlb-troubleshooting-guide&quot; data-og-host=&quot;repost.aws&quot; data-og-description=&quot;This guide focuses on expediting isolation and resolution of incidents involving NLB. This guide will help you gather the right information to troubleshoot NLB issues efficiently.&quot; data-og-title=&quot;Network Load Balancer (NLB) troubleshooting guide&quot; data-og-type=&quot;website&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://repost.aws/ko/articles/ARIFCVXv6-QV2HIvnJ4px1Zw/network-load-balancer-nlb-troubleshooting-guide&quot; data-source-url=&quot;https://repost.aws/ko/articles/ARIFCVXv6-QV2HIvnJ4px1Zw/network-load-balancer-nlb-troubleshooting-guide&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Uya8X/hyUXQ8iApR/t9tKIvtn3T3k2kBYGsf9tk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;Network Load Balancer (NLB) troubleshooting guide&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;This guide focuses on expediting isolation and resolution of incidents involving NLB. This guide will help you gather the right information to troubleshoot NLB issues efficiently.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;repost.aws&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇지만 NLB 쪽 로그를 보니 해당 에러에 대한 의미있어보이는 connection reset이 보이지 않았습니다.&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;&quot;음.. 요청이 많아서 커넥션이 고갈되는 케이스는 아닐텐데.. 그러면 커넥션이 살아있는 시간이 너무 짧게 설정되어있나?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트에서 WebConfig 설정을 어떻게 쓰고 있는가 찾아보니, 설정 값이 없었습니다.&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;WebClient 코드를 뜯어보니 따로 설정하지 않으면 무한하게 유지하게 되어있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;gradle&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;long DEFAULT_POOL_MAX_IDLE_TIME = Long.parseLong(System.getProperty(
       ReactorNetty.POOL_MAX_IDLE_TIME,
       &quot;-1&quot;)); // 무한&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h2 data-ke-size=&quot;size26&quot;&gt;WebClient에 대해서 먼저 이해해야 한다.&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baAUlX/btsGBZ0qVDn/ZlqbZdTxSzGjG5kOrgflTK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baAUlX/btsGBZ0qVDn/ZlqbZdTxSzGjG5kOrgflTK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baAUlX/btsGBZ0qVDn/ZlqbZdTxSzGjG5kOrgflTK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaAUlX%2FbtsGBZ0qVDn%2FZlqbZdTxSzGjG5kOrgflTK%2Fimg.jpg&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;1280&quot; height=&quot;648&quot; data-origin-width=&quot;1280&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;webclient 는 싱글 스레드로 동작합니다. 하나의 이벤트 스레드가 돌면서 요청을 처리하게 됩니다.&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;그리고 Reactor 라는 친구로 구현된 worker 라는 친구에게 요청에 대한 처리가 위임되어 &lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;논블로킹 I/O&lt;/span&gt; 방식으로 처리하게 됩니다. 따라서 메인 스레드가 블로킹되지 않고 다른 작업을 처리할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;WebClient를 사용하여 HTTP 요청을 보낼 때, 실제 HTTP 연결은 이벤트 루프에서 시작됩니다. 요청이 전송되면, 해당 요청에 대한 응답을 처리할 콜백 핸들러가 등록됩니다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #0d0d0d; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 HTTP connection은 event loop에서 시작된다.&lt;/li&gt;
&lt;li&gt;요청이 전송된 후, response callback handler가 등록되고 event loop가 끝난다.&lt;/li&gt;
&lt;li&gt;응답을 받으면 response callback  이 응답을 전달해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;WebClient를 잘 사용하려면, Project Reactor에 대해서도 이해해야 할 것 같습니다.&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;h2 data-ke-size=&quot;size26&quot;&gt;vs RestTemplate? (vs blocking?)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많이 사용하던 web client인 RestTemplate 과 비교해보면 조금 더 이해가 쉬울 것 같습니다.&lt;/p&gt;
&lt;table style=&quot;color: #374151; text-align: left; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: ;&quot;&gt;
&lt;td style=&quot;width: ;height: ;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot;&gt;&lt;b&gt;RestTemplate&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot;&gt;&lt;b&gt;WebClient&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: ;&quot;&gt;
&lt;td style=&quot;width: ;height: ;&quot;&gt;&lt;b&gt;Blocking/Non-blocking&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot;&gt;Blocking&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot;&gt;Non-blocking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: ;&quot;&gt;
&lt;td style=&quot;width: ;height: ;&quot;&gt;&lt;b&gt;Thread Model&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot;&gt;Multi-threaded&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot;&gt;Single-threaded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: ;&quot;&gt;
&lt;td style=&quot;width: ;height: ;&quot;&gt;&lt;b&gt;Performance&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot;&gt;&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;블로킹 호출로 인한 높은 지연&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: ;height: ;&quot;&gt;&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;논블로킹 호출로 인한 낮은 지연&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@boskyjoseph/webclient-vs-resttemplete-48d0a19a23cf&quot;&gt;https://medium.com/@boskyjoseph/webclient-vs-resttemplete-48d0a19a23cf&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://happycloud-lee.tistory.com/m/220&quot;&gt;https://happycloud-lee.tistory.com/m/220&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;성능테스트 결과들을 살펴보면, 동시 요청이 적을때는 비슷한 퍼포먼스를 보여주다가, 동시 요청이 많아질수록 WebClient가 더 우수하다는 것을 알 수 있었습니다.&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 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;h2 data-ke-size=&quot;size26&quot;&gt;실제로 적용해보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운송사에 이미지 업로드 요청을 보내면서 사용하는 요청을 다음과 같이 구성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 형태의 api 요청이 발생할 수 있는 형태라서, blocking 방식의 client 방식보다 훨신 성능상 이점을 볼 수 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;5천건 이상의 요청을 동시에 보내기도 한다.&lt;/li&gt;
&lt;li&gt;요청에 대한 응답이 길면 3초 이상을 넘어가기도 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713097169711&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;SomeApiResponse&amp;gt; request(final String xmlString) {
    return webClient.post()
        .uri(brosUrl)
        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
        .bodyValue(xmlString)
        .retrieve()
        .bodyToMono(SomeApiResponse.class)
        .timeout(Duration.ofSeconds(5))
        .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
        .onErrorResume(e -&amp;gt; {
            log.error(&quot;api error: {}&quot;, e.getMessage(), e);
            final String errorMessage = e.getMessage();
            return Mono.just(createUploadErrorResponse(errorMessage));
        }).doOnNext(response -&amp;gt; log.info(&quot;response: {}&quot;, response));
}&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;p data-ke-size=&quot;size16&quot;&gt;Method chaning 형태로 error 핸들링 및 retry 가 적용이 가능해서 사용성이 아주 좋았던 것 같습니다.&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;h2 data-ke-size=&quot;size26&quot;&gt;failed: Connection reset by peer; 어떻게 해결할까&lt;/h2&gt;
&lt;pre id=&quot;code_1713097580767&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private ConnectionProvider createConnectionProvider() {
    return ConnectionProvider.builder(&quot;webClientConnectionProvider&quot;)
    ///
            .maxIdleTime(Duration.ofSeconds(MAX_CONNECTION_LIFE_TIME)) // 사용하지 않는 상태 유지 시간
            .maxLifeTime(Duration.ofSeconds(MAX_CONNECTION_LIFE_TIME)) // Connection Pool의 Connection 유효 시간
            .evictInBackground(Duration.ofSeconds(MAX_CONNECTION_LIFE_TIME)) // Connection Pool의 Connection 유효 시간이 만료된 Connection을 백그라운드에서 제거
            .lifo()
            .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 상대방 서버 쪽에서 설정한 timeout 이 몇 초로 설정되어 있는가가 중요한 문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 MAX_CONNECITON_LIFE_TIME 을 설정해서 커넥션의 유효시간을 상대 서버보다 조금 낮고 일정하게 설정하여 해결했습니다.&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;Reactor 에 대해서 조금 더 공부해보고, 조금 더 디테일한 내용을 작성해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용이 낮설어서 공부하는데에 한참 걸리네요 ^^;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>깻묵(천진우)</author>
      <guid isPermaLink="true">https://netal.tistory.com/115</guid>
      <comments>https://netal.tistory.com/entry/Spring-WebClient-failed-Connection-reset-by-peer-%ED%95%B4%EA%B2%B0%ED%95%B4%EB%B3%B4%EA%B8%B0#entry115comment</comments>
      <pubDate>Sun, 14 Apr 2024 21:34:21 +0900</pubDate>
    </item>
    <item>
      <title>Trunk Based Development 에서 Feature Flag 경험하기</title>
      <link>https://netal.tistory.com/entry/Trunk-Based-Development-%EC%97%90%EC%84%9C-Feature-Flag-%EA%B2%BD%ED%97%98%ED%95%98%EA%B8%B0</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;기능을 개발했는데, 이 기능은 언제 배포되나요?&quot;&lt;br /&gt;xxx: &quot;그냥 merge 하시면 바로 배포됩니다.&quot;&lt;br /&gt;&quot;배포가 바로 된다구요?&quot;&lt;br /&gt;xxx: &quot;네&quot;&lt;br /&gt;&quot;음? 스테이징도 없고 QA도 없어요?&quot;&lt;br /&gt;xxx: &quot;네&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-03-02 오후 1.02.23.png&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/loSzb/btsFlWZaw4R/RqVqiEbJskKtPzNLyPP0tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/loSzb/btsFlWZaw4R/RqVqiEbJskKtPzNLyPP0tk/img.png&quot; data-alt=&quot;예전 배포 프로세스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/loSzb/btsFlWZaw4R/RqVqiEbJskKtPzNLyPP0tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FloSzb%2FbtsFlWZaw4R%2FRqVqiEbJskKtPzNLyPP0tk%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;1492&quot; height=&quot;500&quot; data-filename=&quot;스크린샷 2024-03-02 오후 1.02.23.png&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예전 배포 프로세스&lt;/figcaption&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;하나의 기능이 나가기 위해서 수많은 confirm 이 필요했고&amp;nbsp;QA 조직이 꽤나 크게 존재했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;직접 경험한 문제들&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문제 1. 배포 주기가 너무 길어서, 피드백이 느리다&lt;/li&gt;
&lt;li&gt;문제 2. 피드백에 대한 대응이 느리다&lt;/li&gt;
&lt;li&gt;문제 3. 우선적으로 구현해야하는 기능이 있다면, 병목이 너무 길어진다&lt;/li&gt;
&lt;li&gt;문제 4. 기획자와 QA가 생각하는 기능이 다르다면 개발자의 등이 터져버린다 !&lt;/li&gt;
&lt;li&gt;문제 5. 모든게 길어지다 보니 작업 단위가 커지게 되고, 기능에 대한 리뷰나 코드에 대한 리뷰가 점점 어려워진다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;물론 이런 장점은 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점 1. 안정성이 보장된다&lt;/li&gt;
&lt;li&gt;장점 2. 최종적인 배포는 어느정도 이상적인 방향으로 합의된다. (아마도)&lt;/li&gt;
&lt;li&gt;장점 3. 여러명이 작업해도 코드 베이스에 큰 문제가 발생하지 않는다.&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;Trunk Based Development 경험하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;진우님, 저희는 Trunk Based Development 를 사용해요&quot;&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;새로운 팀으로 오니, 개발 조직의 인원은 비슷한데 QA가 따로 없었습니다.&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;스크린샷 2024-03-02 오후 1.21.51.png&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AYkDU/btsFoeEHljZ/XaQSoRkWHPydacVZiwGvdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AYkDU/btsFoeEHljZ/XaQSoRkWHPydacVZiwGvdk/img.png&quot; data-alt=&quot;새로운 조직의 배포 프로세스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AYkDU/btsFoeEHljZ/XaQSoRkWHPydacVZiwGvdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAYkDU%2FbtsFoeEHljZ%2FXaQSoRkWHPydacVZiwGvdk%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;475&quot; height=&quot;283&quot; data-filename=&quot;스크린샷 2024-03-02 오후 1.21.51.png&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;새로운 조직의 배포 프로세스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획자가 기획을 주면 개발자는 빠르게 개발하고, 기획자에게 바로 피드백을 받을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조직에 합류하고 단지 한 번의 기능만 개발해봤을 뿐인데, 굉장히 감동적이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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-filename=&quot;blob&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QwsMx/btsFoEXBvIW/QhTwYMeuszzufxuHVY63K0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QwsMx/btsFoEXBvIW/QhTwYMeuszzufxuHVY63K0/img.png&quot; data-alt=&quot;에자일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QwsMx/btsFoEXBvIW/QhTwYMeuszzufxuHVY63K0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQwsMx%2FbtsFoEXBvIW%2FQhTwYMeuszzufxuHVY63K0%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;415&quot; height=&quot;303&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;에자일&lt;/figcaption&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;장점 1: 요구사항에 대해서 대응이 빠르다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점 2: 개발 완료 즉시 피드백이 가능하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점 3: 프로덕트에 대해 이해가 높은 기획자가 있다면 요구사항의 목적성이 분명하기 때문에 기획과 리뷰가 명확하다&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;그리고 TDD ~ 리팩토링을 하기에도 아주 용이했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;명시된 요구사항을 그대로 테스트로 옮긴다&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://deviq.com/antipatterns/duct-tape-coder&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;코드 품질이 엉망이더라도&lt;/a&gt; 테스트가 통과할 수 있는 절차지향적인 코드로 해결한다 (문제 이해 과정)&lt;/li&gt;
&lt;li&gt;과감하게 Rollback or Merge&lt;/li&gt;
&lt;li&gt;요구사항 맞는지 Feedback 후에 Refactorying&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Feature Flag를 활용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 개발에 있어서 안정성은 어떻게 할까요? QA가 따로 없다 보니 꼼꼼한 테스트는 힘들지 않을까요?&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;이런 기존 기능들을 안전하게 변경하고 배포하거나 방법으로 Feature Flag를 사용하고 있습니다.&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;318&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyCwbf/btsFqW4pTQR/6T0AdkR2wMFGjirVXLrPu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyCwbf/btsFqW4pTQR/6T0AdkR2wMFGjirVXLrPu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyCwbf/btsFqW4pTQR/6T0AdkR2wMFGjirVXLrPu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyCwbf%2FbtsFqW4pTQR%2F6T0AdkR2wMFGjirVXLrPu1%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;318&quot; height=&quot;159&quot; data-origin-width=&quot;318&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 뿐만 아니라, 특정 고객 / 조건별로 조건을 부여해서 기능을 제공할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://martinfowler.com/bliki/FeatureFlag.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;마틴 파울러의 글에 feature flag 의 다양한 활용에 대해서&amp;nbsp; 다음과 같은 내용이 있습니다.&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;지금까지 기능 플래그를 부분적으로 구축된 기능을 숨기는 데 사용하는 것으로 설명했습니다. 일종의 기능 플래그를&amp;nbsp;&lt;br /&gt;릴리스 토글이라고&amp;nbsp;부릅니다 .&amp;nbsp;Hodgson은 또한&amp;nbsp;A/B 테스트를 위한&amp;nbsp;&lt;br /&gt;실험 토글 , 운영 직원에게 제어 기능을 제공하는 작업 토글&amp;nbsp;,&amp;nbsp;다양한 사용자 하위 집합에 대한 기능 액세스를 제어하는&amp;nbsp;&lt;br /&gt;​​권한 토글을 식별합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 테스트도 없는 레거시 코드가 있다고 가정해봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1709355714784&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Feature {
    
    private final ExternalService externalService;

    public void feature(DomainObject domainObject, int delta) {
        domainObject.addValue(delta);
        domainObject.addValue(delta);
        domainObject.addValue(delta);
        domainObject.addValue(delta);
        externalService.doSomthing(); // Unit 테스트 불가능
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 레거시 코드를 변경하고 테스트 하는 것은 까다로운 일입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 만들기 어렵고, 테스트를 작성하더라도 운영 중에 어떤 사이드 이팩트가 있을지에 대해서 확신하지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴때 Feature Flag를 쓰면 안심하고 배포할 수 있습니다&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;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 Feature {

    private final ExternalService externalService;

    public void feature(DomainObject domainObject, int delta) {
        if (domainObject.isFeatureFlagActive()) {
            // 새로운 로직
            domainObject.newOperation(delta);
        } else {
    		// 기존 로직
            domainObject.addValue(delta);
            domainObject.addValue(delta);
            domainObject.addValue(delta);
            domainObject.addValue(delta);
        }
        externalService.doSomthing();
    }
}

class TestClass {
    @Test //유닛테스트로 새로운 로직만 검증
    public void testFeature() {
        // Given
        final DomainObject domainObject = new DomainObject();
        final int delta = 1;
        // When
        domainObject.newOperation(delta);
        // Then
        final int expected;
        assertThat(domainObject.getDelta()).isEqualTo(expected);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;Feature Flag를 database에서 조회하고, 해당 flag가 활성화 되어 있다면 새로운 기능을 반영한다.&lt;/li&gt;
&lt;li&gt;만약 문제가 생기면 database에서 직접 update 해서 런타임에 기능을 비활성화 한다.&lt;/li&gt;
&lt;li&gt;문제가 없다면 다음 배포에서 해당 코드를 제거 및 리팩토링 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.atlassian.com/solutions/devops/integrations/feature-flags&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.atlassian.com/solutions/devops/integrations/feature-flags&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://martinfowler.com/bliki/FeatureFlag.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://martinfowler.com/bliki/FeatureFlag.html&lt;/a&gt;&lt;/p&gt;</description>
      <author>깻묵(천진우)</author>
      <guid isPermaLink="true">https://netal.tistory.com/114</guid>
      <comments>https://netal.tistory.com/entry/Trunk-Based-Development-%EC%97%90%EC%84%9C-Feature-Flag-%EA%B2%BD%ED%97%98%ED%95%98%EA%B8%B0#entry114comment</comments>
      <pubDate>Sat, 2 Mar 2024 14:15:20 +0900</pubDate>
    </item>
    <item>
      <title>서평 - 단위 테스트 (Unit Testing)</title>
      <link>https://netal.tistory.com/entry/%EC%84%9C%ED%8F%89-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-Unit-Testing</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;1867&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bW92zW/btsEYMvf6A3/2TCsinND5SHaLIcHXDVIs0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bW92zW/btsEYMvf6A3/2TCsinND5SHaLIcHXDVIs0/img.jpg&quot; data-alt=&quot;대구 놀러갔다가 획득한 마지막 재고&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bW92zW/btsEYMvf6A3/2TCsinND5SHaLIcHXDVIs0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbW92zW%2FbtsEYMvf6A3%2F2TCsinND5SHaLIcHXDVIs0%2Fimg.jpg&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;502&quot; height=&quot;669&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;1867&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대구 놀러갔다가 획득한 마지막 재고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떻게 읽게 되었나?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 회사로 합류하게 되면서, 같이 업무를 진행하실 분께 여쭤보았습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;합류 전까지 어떤 책을 읽고 합류하면 좋을까요?&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 추천받은 책이 이 단위 테스트  책입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&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;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://enterprisecraftsmanship.com/&quot;&gt;블라디미르 코리코프&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&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;/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;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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서평&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책을 읽는 시점이 지금보다 가장 좋은 순간은 없었을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 회사에서 어마어마한 레거시 코드와 싸우며 테스트와 관련된 수많은 이슈를 마주했었기 때문인데요,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 커버리지 20%&lt;/li&gt;
&lt;li&gt;@Disabled 처리된 수많은 통합 테스트&lt;/li&gt;
&lt;li&gt;무분별한 mocking&lt;/li&gt;
&lt;li&gt;알아보기 힘든 테스트&lt;/li&gt;
&lt;li&gt;SpringBootTest로 인한 너무 느린 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2년 이상 활발하게 개발되고 있는 프로젝트였으며 꽤나 코드의 볼륨이 컸습니다. 그래서 기능 추가나 변경 하나하나가 두려움의 연속이었고, 일일이 관련된 사이드 이팩트들을 확인해야 했기 때문에 cost 와 risk 가 함께 기하급수적으로 올라가는 불행한 상황이었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 항상 해당 프로젝트에서 테스트를 어떻게 뜯어고치는지가 항상 고민거리였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런저런 방법들을 고민해서 테스트 방식들을 도입하고, 결국 코드 베이스까지 다시 점진적으로 리팩토링하는 하는 형태로 다가갔는데 추상적인 접근 방법으로는 시행착오가 너무 많았고 한계가 있었습니다. (물론 많은 것을 배우는 시간이었지만요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdKeE8/btsE1EQh4PV/FYmXg9kBcLUEmx5Qy1ZNs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdKeE8/btsE1EQh4PV/FYmXg9kBcLUEmx5Qy1ZNs1/img.png&quot; data-alt=&quot;https://lostechies.com/derickbailey/2010/11/11/cost-vs-risk-in-testing/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdKeE8/btsE1EQh4PV/FYmXg9kBcLUEmx5Qy1ZNs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdKeE8%2FbtsE1EQh4PV%2FFYmXg9kBcLUEmx5Qy1ZNs1%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;640&quot; height=&quot;341&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://lostechies.com/derickbailey/2010/11/11/cost-vs-risk-in-testing/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;특히 이 책의 전반에서 강요하는 다음 개념들이 너무나도 마음에 들었습니다. 이 개념은 꼭 외워두고 다양한 개념들에 적용해도 좋을 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbHiky/btsE1E3Odin/MZ3jVI1xY2bS1nq86GSop0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbHiky/btsE1E3Odin/MZ3jVI1xY2bS1nq86GSop0/img.jpg&quot; data-origin-width=&quot;326&quot; data-origin-height=&quot;155&quot; data-is-animation=&quot;false&quot; style=&quot;width: 60.1942%; margin-right: 10px;&quot; data-widthpercent=&quot;60.9&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbHiky/btsE1E3Odin/MZ3jVI1xY2bS1nq86GSop0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbHiky%2FbtsE1E3Odin%2FMZ3jVI1xY2bS1nq86GSop0%2Fimg.jpg&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;326&quot; height=&quot;155&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tnD48/btsE1BeVyIz/SX73FXjthdURRkTa138Lfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tnD48/btsE1BeVyIz/SX73FXjthdURRkTa138Lfk/img.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;948&quot; data-is-animation=&quot;false&quot; style=&quot;width: 38.643%;&quot; data-widthpercent=&quot;39.1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tnD48/btsE1BeVyIz/SX73FXjthdURRkTa138Lfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtnD48%2FbtsE1BeVyIz%2FSX73FXjthdURRkTa138Lfk%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;1280&quot; height=&quot;948&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음주에는 미디엄 블로그에 이 책에서 다루는 내용을 item으로 예제와 함께 정리해서 올려보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;</description>
      <author>깻묵(천진우)</author>
      <guid isPermaLink="true">https://netal.tistory.com/113</guid>
      <comments>https://netal.tistory.com/entry/%EC%84%9C%ED%8F%89-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-Unit-Testing#entry113comment</comments>
      <pubDate>Sun, 18 Feb 2024 15:23:25 +0900</pubDate>
    </item>
    <item>
      <title>Heap Dump로 Memory Leak 분석하고 해결하기 (H2 CacheLongKeyLIRS memory leak)</title>
      <link>https://netal.tistory.com/entry/heap-dump%EB%A1%9C-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%A6%AD-%EB%B6%84%EC%84%9D%ED%95%98%EA%B3%A0-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-H2-CacheLongKeyLIRS-memory-leak</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 컨플루언스에 히스토리 저장 목적으로 작성했던 글을 가져왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 분석과 해결 과정을 나타낸 글이라서, 두서가 없는 글입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가볍게 읽어주세요 :)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;환경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS EC2 or ompremise&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 17&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 2.7&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;H2 Database 1.4.200 file write mode&lt;/p&gt;
&lt;pre id=&quot;code_1704624797747&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jdbc:h2:file:/data/happy;CACHE_SIZE=2097152;WRITE_DELAY=300;DB_CLOSE_DELAY=30;DB_CLOSE_ON_EXIT=FALSE;AUTO_SERVER=TRUE;&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;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-filename=&quot;스크린샷 2024-01-07 오후 7.13.12.png&quot; data-origin-width=&quot;419&quot; data-origin-height=&quot;33&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOK5uT/btsC9Abc4QM/fvedhnLwcAYIi7W4daHKd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOK5uT/btsC9Abc4QM/fvedhnLwcAYIi7W4daHKd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOK5uT/btsC9Abc4QM/fvedhnLwcAYIi7W4daHKd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOK5uT%2FbtsC9Abc4QM%2FfvedhnLwcAYIi7W4daHKd0%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;419&quot; height=&quot;33&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.13.12.png&quot; data-origin-width=&quot;419&quot; data-origin-height=&quot;33&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리가 지속적으로 증가하고, 지속적으로 스프링 서버가 죽는다는 제보를 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 도커 컨테이너는 4코어 메모리 2GB 환경에서 실행되고 있었고, 처리량과 서버 스팩을 생각해보면 꽤나 여유있는 환경이었습니다.&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;그렇지만 도커 컨테이너로 실행되던 jvm 이 엄청나게 많은 메모리를 차지하고 있었고, QA 환경에서 동일 환경으로 구성하여 재현을 시도했습니다.&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;edited_스크린샷 2024-01-07 오후 7.12.06.png&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vNkRg/btsC9BHWZ28/Hj0hgZghCugDpQBK1aQXI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vNkRg/btsC9BHWZ28/Hj0hgZghCugDpQBK1aQXI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vNkRg/btsC9BHWZ28/Hj0hgZghCugDpQBK1aQXI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvNkRg%2FbtsC9BHWZ28%2FHj0hgZghCugDpQBK1aQXI1%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;920&quot; height=&quot;238&quot; data-filename=&quot;edited_스크린샷 2024-01-07 오후 7.12.06.png&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;238&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;실제로 3시간만에 메모리 사용량이 40% -&amp;gt; 90%까지 올라갔고, 혹시나 절대적인 메모리 부족 현상 때문에 그런가 싶어서 4GB로 올려봤지만, 동일한 현상이 재현되었습니다.&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 id=&quot;메모리-덤프-뜨는-방법&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;29&quot; data-ke-size=&quot;size23&quot;&gt;메모리 덤프를 떠보자&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;이런저런 옵션들을 바꿔보고 고민하다가, 결국 예상가는 문제점이 떠오르지 않아서 메모리 덤프를 시도했습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;메모리 덤프는 jmap을 통해서 떠볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;docker exec -it 33d1dbbfd7ab sh&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;76&quot; data-ke-size=&quot;size16&quot;&gt;jhsdb jmap --pid 1&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;76&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704621571073&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Attaching to process ID 1, please wait...
ERROR: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permitted
Error attaching to process: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permitted
sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permitted
	at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.execute(LinuxDebuggerLocal.java:187)
	at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach(LinuxDebuggerLocal.java:345)
	at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.attachDebugger(HotSpotAgent.java:651)
	at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.setupDebuggerLinux(HotSpotAgent.java:589)
	at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.setupDebugger(HotSpotAgent.java:368)
	at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:337)
	at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:142)
	at jdk.hotspot.agent/sun.jvm.hotspot.tools.Tool.start(Tool.java:203)
	at jdk.hotspot.agent/sun.jvm.hotspot.tools.Tool.execute(Tool.java:134)
	at jdk.hotspot.agent/sun.jvm.hotspot.tools.JMap.main(JMap.java:202)
	at jdk.hotspot.agent/sun.jvm.hotspot.SALauncher.runJMAP(SALauncher.java:340)
	at jdk.hotspot.agent/sun.jvm.hotspot.SALauncher.main(SALauncher.java:500)
Caused by: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permitted
	at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach0(Native Method)
	at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$1AttachTask.doit(LinuxDebuggerLocal.java:336)
	at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.run(LinuxDebuggerLocal.java:162)&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;a href=&quot;https://stackoverflow.com/questions/2913948/jmap-cant-connect-to-make-a-dump&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/2913948/jmap-cant-connect-to-make-a-dump&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704621615579&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Jmap can't connect to make a dump&quot; data-og-description=&quot;We have an open beta of an app which occasionally causes the heapspace to overflow. The JVM reacts by going on a permanent vacation. To analyze this I would like to peek into the memory at the po...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/2913948/jmap-cant-connect-to-make-a-dump&quot; data-og-url=&quot;https://stackoverflow.com/questions/2913948/jmap-cant-connect-to-make-a-dump&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Qz3Vw/hyUXOxr6lr/OVCAq9qnHBCn4mejRn7BY0/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/2913948/jmap-cant-connect-to-make-a-dump&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/2913948/jmap-cant-connect-to-make-a-dump&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Qz3Vw/hyUXOxr6lr/OVCAq9qnHBCn4mejRn7BY0/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Jmap can't connect to make a dump&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;We have an open beta of an app which occasionally causes the heapspace to overflow. The JVM reacts by going on a permanent vacation. To analyze this I would like to peek into the memory at the po...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;메모리 덤프를 뜨려고 시도했으나, docker 환경에서는 &lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;해당 프로세스를 실행한 실행한 사용자 이름이 달라서 덤프를 뜰 수 없다고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;docker-에서-메모리-덤프를-뜨는-방법&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2052&quot; data-ke-size=&quot;size23&quot;&gt;docker 에서 메모리 덤프를 뜨는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704622733888&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker exec 5593e3c179bb jcmd 1 GC.heap_dump /tmp/center4.hprof -all dump&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굿! 잘됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 -all 옵션을 붙여야 GC 를 실행하지 않은 상태로 가져옵니다.&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;종종 메모리가 정리되는 케이스가 있었기 때문에, 혹시라도 GC 가 적절한 타이밍에 못 도는 것인가? 라는 가설도 있었기에 GC를 돌리지 않고 힙 메모리를 그대로 가져왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704622849850&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 도커 컨테이너에서 꺼내기
docker cp 5593e3c179bb:/tmp/dump.hprof /home/ubuntu/memory_dump/dump.hprof

// 내 로컬로 가져오기
sudo scp -i /Users/user/workspace/key/.pem ubuntu@43.201.88.41:/home/ubuntu/memory_dump/dump.hprof /Users/user/workspace/memory_dump&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;Hprof 파일을 로컬로 들고왔고, 1.2GB 크기였습니다.&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 id=&quot;MAT-분석-툴-열기&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2464&quot; data-ke-size=&quot;size23&quot;&gt;Eclipse MAT 메모리 분석 툴 열기&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;a href=&quot;https://eclipse.dev/mat/downloads.php&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://eclipse.dev/mat/downloads.php&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704623627182&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation&quot; data-og-description=&quot;The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 415 open source projects, including runtimes, tools and frameworks.&quot; data-og-host=&quot;eclipse.dev&quot; data-og-source-url=&quot;https://eclipse.dev/mat/downloads.php&quot; data-og-url=&quot;https://eclipse.dev/mat/downloads.php&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/XVQzw/hyU2jP4Yx8/3TsLBquXTx9NsQiqdK54Ek/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/i9idG/hyU2ow4Bu5/121O4iQ7LXZ0kZQfviKMw1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400&quot;&gt;&lt;a href=&quot;https://eclipse.dev/mat/downloads.php&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://eclipse.dev/mat/downloads.php&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/XVQzw/hyU2jP4Yx8/3TsLBquXTx9NsQiqdK54Ek/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/i9idG/hyU2ow4Bu5/121O4iQ7LXZ0kZQfviKMw1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 415 open source projects, including runtimes, tools and frameworks.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;eclipse.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mac 에서 설치하는 경우, application 폴더로 옮기고 열어야합니다.&lt;/li&gt;
&lt;li&gt;메모리 할당도 늘리고 작업하는 것을 추천합니다. MAT이 OOM 뜨더라구요..
&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://jojoldu.tistory.com/738&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jojoldu.tistory.com/738&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;덤프 파일 권한 변경해줘야합니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;open a heap dump 메뉴를 통해서 원하는 힙 덤프 파일을 열어줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.34.11.png&quot; data-origin-width=&quot;511&quot; data-origin-height=&quot;99&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6Del8/btsC4cJHI3U/hAKv8GoGmTydH9eqoVKwK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6Del8/btsC4cJHI3U/hAKv8GoGmTydH9eqoVKwK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6Del8/btsC4cJHI3U/hAKv8GoGmTydH9eqoVKwK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6Del8%2FbtsC4cJHI3U%2FhAKv8GoGmTydH9eqoVKwK0%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;511&quot; height=&quot;99&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.34.11.png&quot; data-origin-width=&quot;511&quot; data-origin-height=&quot;99&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 / 비정상 시점을 나눠서 5번 정도 진행했는데, 비정상적인 메모리 시점에서는 m2 프로세스 기준 7시간이 소요됐습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.36.34.png&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAlVZN/btsC6VULD2P/rL0QwsKRjDODiZGk5hkEcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAlVZN/btsC6VULD2P/rL0QwsKRjDODiZGk5hkEcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAlVZN/btsC6VULD2P/rL0QwsKRjDODiZGk5hkEcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAlVZN%2FbtsC6VULD2P%2FrL0QwsKRjDODiZGk5hkEcK%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;577&quot; height=&quot;365&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.36.34.png&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;365&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;덤프가 완료되면 mat 에서 자동 분석 결과를 추천해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 릭이 의심되는 상황이니, Leak suspects report 를 열어봅시다.&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 id=&quot;객체-구경하기&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2619&quot; data-ke-size=&quot;size23&quot;&gt;본격적으로 객체 분석하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.37.34.png&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Yz8kV/btsC9aw4sil/3aJbknwUKr4kHwigV8TYM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Yz8kV/btsC9aw4sil/3aJbknwUKr4kHwigV8TYM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Yz8kV/btsC9aw4sil/3aJbknwUKr4kHwigV8TYM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYz8kV%2FbtsC9aw4sil%2F3aJbknwUKr4kHwigV8TYM1%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;682&quot; height=&quot;456&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.37.34.png&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;456&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;overview 에서 이미 비정상적인 객체가 탐지되었다고 합니다.&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;실제로 spring boot 가 사용하는 메모리는 작았고, H2 프로세서를 사용하면서 나오는 메모리 비용이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 디테일하게 해당 정보를 찾아보면,,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.38.21.png&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3jZNS/btsDbd7U2N9/JlkZKzOlVOgD1RIsI31Omk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3jZNS/btsDbd7U2N9/JlkZKzOlVOgD1RIsI31Omk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3jZNS/btsDbd7U2N9/JlkZKzOlVOgD1RIsI31Omk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3jZNS%2FbtsDbd7U2N9%2FJlkZKzOlVOgD1RIsI31Omk%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;756&quot; height=&quot;280&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.38.21.png&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CacheLongKeyLIRS 라는 이름의 객체가 6억~15억 byte 정도 생성된 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-01-07 오후 7.40.42.png&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyJLG9/btsC8UOzH1B/BjvpJydALmf2kbiK9T8X90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyJLG9/btsC8UOzH1B/BjvpJydALmf2kbiK9T8X90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyJLG9/btsC8UOzH1B/BjvpJydALmf2kbiK9T8X90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyJLG9%2FbtsC8UOzH1B%2FBjvpJydALmf2kbiK9T8X90%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;326&quot; height=&quot;442&quot; data-filename=&quot;edited_스크린샷 2024-01-07 오후 7.40.42.png&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;879&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;실제로 구글링을 해보니 h2 에 메모리 릭 이슈가 잦은 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 저희는 비즈니스 특성상 h2를 사용해야했고, 문제 해결 방법을 찾기 시작했습니다.&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;h2 data-ke-size=&quot;size26&quot;&gt;CacheLongKeyLIRS: H2의 캐시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://javadoc.io/doc/com.h2database/h2/1.4.192/org/h2/mvstore/cache/CacheLongKeyLIRS.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://javadoc.io/doc/com.h2database/h2/1.4.192/org/h2/mvstore/cache/CacheLongKeyLIRS.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704624191636&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;CacheLongKeyLIRS - h2 1.4.192 javadoc&quot; data-og-description=&quot;Latest version of com.h2database:h2 https://javadoc.io/doc/com.h2database/h2 Current version 1.4.192 https://javadoc.io/doc/com.h2database/h2/1.4.192 package-list path (used for javadoc generation -link option) https://javadoc.io/doc/com.h2database/h2/1.4.&quot; data-og-host=&quot;javadoc.io&quot; data-og-source-url=&quot;https://javadoc.io/doc/com.h2database/h2/1.4.192/org/h2/mvstore/cache/CacheLongKeyLIRS.html&quot; data-og-url=&quot;https://javadoc.io/doc/com.h2database/h2/1.4.192/org/h2/mvstore/cache/CacheLongKeyLIRS.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://javadoc.io/doc/com.h2database/h2/1.4.192/org/h2/mvstore/cache/CacheLongKeyLIRS.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://javadoc.io/doc/com.h2database/h2/1.4.192/org/h2/mvstore/cache/CacheLongKeyLIRS.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;CacheLongKeyLIRS - h2 1.4.192 javadoc&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Latest version of com.h2database:h2 https://javadoc.io/doc/com.h2database/h2 Current version 1.4.192 https://javadoc.io/doc/com.h2database/h2/1.4.192 package-list path (used for javadoc generation -link option) https://javadoc.io/doc/com.h2database/h2/1.4.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;javadoc.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;문서를 읽어보니 얼추 다른 RDBMS 의 Buffer Pool과 비슷한 역할을 하는 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CHw1b/btsC32mPlg7/SG9RkdWkQkJ5RXX4a8IQnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CHw1b/btsC32mPlg7/SG9RkdWkQkJ5RXX4a8IQnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CHw1b/btsC32mPlg7/SG9RkdWkQkJ5RXX4a8IQnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCHw1b%2FbtsC32mPlg7%2FSG9RkdWkQkJ5RXX4a8IQnk%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;421&quot; height=&quot;323&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 요놈이 무슨 이렇게 많은 메모리를 들고 있는걸까요?&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 - 캐시 사용량 제한하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 사용량을 제한해야겠다는 생각이 들어서 docker-compose 를 열었더니, 설정값을 보고 놀랐습니다.&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;u&gt;&lt;b&gt;CACHE_SIZE=2097152&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLfvEx/btsC7uo6jYN/ApEneSiDzenFsgYwnCXFW0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLfvEx/btsC7uo6jYN/ApEneSiDzenFsgYwnCXFW0/img.jpg&quot; data-alt=&quot;에..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLfvEx/btsC7uo6jYN/ApEneSiDzenFsgYwnCXFW0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLfvEx%2FbtsC7uo6jYN%2FApEneSiDzenFsgYwnCXFW0%2Fimg.jpg&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;259&quot; height=&quot;194&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;에..&lt;/figcaption&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;해당 도커 컨테이너에 할당된 메모리가 2GB 인데, 캐시 사이즈가 2GB로 설정되어 있었습니다.&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_1704624995570&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET CACHE_SIZE 8192&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;일단 캐시 사이즈를 작게 가져가고, 문제 재현이 되지 않으면 적절한 캐시 사이즈를 튜닝해야겠다는 생각으로 8mb 만 설정했습니다.&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;그러나 문제는 해결되지 않았고 또다시 메모리 95%를 찍고 있었습니다.&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 id=&quot;의심케이스1---트랜잭션-or-커넥션을-닫는-것에-실패하는-케이스가-있나?&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2915&quot; data-ke-size=&quot;size23&quot;&gt;의심케이스2 - 트랜잭션 or 커넥션을 닫는 것에 실패하는 케이스가 있나?&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2962&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://groups.google.com/g/h2-database/c/PlcWbtEOvI0/m/qEaWkHTaAQAJ?utm_medium=email&amp;amp;utm_source=footer&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://groups.google.com/g/h2-database/c/PlcWbtEOvI0/m/qEaWkHTaAQAJ?utm_medium=email&amp;amp;utm_source=footer&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2962&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭션이 닫히지 않으면 해당 캐시들이 GC 가 되지 않는다고 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3001&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3003&quot; data-ke-size=&quot;size16&quot;&gt;추측: 그렇다면 어딘가 트랜잭션을 닫는 것에 실패한 케이스가 누적되고 있으며, 어쩌다가 메모리 사용량이 다시 뚝 떨어지는거는 해당 커넥션 or 트랜잭션이 자동으로 정리된건가?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3102&quot; 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;스크린샷 2024-01-07 오후 7.59.42.png&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekdsue/btsC2whnHRU/anCAnxERcbxQhYUFFGsMZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekdsue/btsC2whnHRU/anCAnxERcbxQhYUFFGsMZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekdsue/btsC2whnHRU/anCAnxERcbxQhYUFFGsMZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fekdsue%2FbtsC2whnHRU%2FanCAnxERcbxQhYUFFGsMZk%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;801&quot; height=&quot;308&quot; data-filename=&quot;스크린샷 2024-01-07 오후 7.59.42.png&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3102&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3102&quot; data-ke-size=&quot;size16&quot;&gt;예전부터 찝찝한 놈들이 있는데, 혹시 얘들때문인가&amp;hellip;?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3153&quot; data-ke-size=&quot;size16&quot;&gt;항상 정체 모를 친구들이 무의미하게 커넥션을 물고 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3186&quot; data-ke-size=&quot;size16&quot;&gt;이거는 왜 생기는거지?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3200&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3202&quot; data-ke-size=&quot;size16&quot;&gt;커넥션 풀 때문인 것으로 일단 추측. 그런데 그런 것 치고는 너무 적지 않나&amp;hellip;?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3248&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Nop. 풀 설정이 minimum-idle: 5 라서 적을 수 있음.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3248&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;의심케이스2---LOB-데이터를-처리할-때-캐시에서-처리하고-반환하지-않는건가?&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3292&quot; data-ke-size=&quot;size23&quot;&gt;의심케이스3 - LOB 데이터를 처리할 때 캐시에서 처리하고 default 시간동안 반환되지 않는건가?&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;a href=&quot;https://groups.google.com/g/h2-database/c/PlcWbtEOvI0/m/qEaWkHTaAQAJ?utm_medium=email&amp;amp;utm_source=footer&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://groups.google.com/g/h2-database/c/PlcWbtEOvI0/m/qEaWkHTaAQAJ?utm_medium=email&amp;amp;utm_source=footer&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://groups.google.com/g/h2-database/c/eXBzpF4WnNk&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://groups.google.com/g/h2-database/c/eXBzpF4WnNk&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704625241518&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;In-Memory H2 deleted rows not garbage collected.&quot; data-og-description=&quot;Look at the above charts, how memory usage is increasing over the time, even if database has less records. In the beginning Db has 100k records and the memory consumption is around 50%. As data gets ingested and deleted , over the time the memory usage inc&quot; data-og-host=&quot;groups.google.com&quot; data-og-source-url=&quot;https://groups.google.com/g/h2-database/c/PlcWbtEOvI0/m/qEaWkHTaAQAJ?utm_medium=email&amp;amp;utm_source=footer&quot; data-og-url=&quot;https://groups.google.com/g/h2-database/c/PlcWbtEOvI0/m/qEaWkHTaAQAJ?utm_medium=email&amp;amp;utm_source=footer&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://groups.google.com/g/h2-database/c/PlcWbtEOvI0/m/qEaWkHTaAQAJ?utm_medium=email&amp;amp;utm_source=footer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://groups.google.com/g/h2-database/c/PlcWbtEOvI0/m/qEaWkHTaAQAJ?utm_medium=email&amp;amp;utm_source=footer&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;In-Memory H2 deleted rows not garbage collected.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Look at the above charts, how memory usage is increasing over the time, even if database has less records. In the beginning Db has 100k records and the memory consumption is around 50%. As data gets ingested and deleted , over the time the memory usage inc&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;groups.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;I have that feeling that you just run queries with LOB in the result which causes extensive LOB cloning. Try to add ;LOB_TIMEOUT=1000 (1 sec instead of default 5 min)&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;위의 논의를 보면, lob 데이터를 매니징하면서 이 데이터의 처리를 위해서 내부적으로 clone을 하고 있나봅니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3587&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이 clone을 했던 것이 계속해서 캐시에 쌓이고, 해당 캐시가 정리되는 default 시간이 5분이라서 지워지는데에 시간이 좀 걸리는걸까요?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3732&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3822&quot; data-ke-size=&quot;size16&quot;&gt;그러면 LOB_TIMEOUT=1000 해서 1초로 걸어보면 문제가 해결되는걸까요??????  &lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3822&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3822&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1704625358229&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Advanced&quot; data-og-description=&quot;&amp;nbsp; Advanced Result Sets Large Objects Linked Tables Spatial Features Recursive Queries Updatable Views Transaction Isolation Multi-Version Concurrency Control (MVCC) Clustering / High Availability Two Phase Commit Compatibility Keywords / Reserved Words St&quot; data-og-host=&quot;h2database.com&quot; data-og-source-url=&quot;https://h2database.com/html/advanced.html#large_objects&quot; data-og-url=&quot;https://h2database.com/html/advanced.html#large_objects&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://h2database.com/html/advanced.html#large_objects&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://h2database.com/html/advanced.html#large_objects&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Advanced&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; Advanced Result Sets Large Objects Linked Tables Spatial Features Recursive Queries Updatable Views Transaction Isolation Multi-Version Concurrency Control (MVCC) Clustering / High Availability Two Phase Commit Compatibility Keywords / Reserved Words St&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;h2database.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3822&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3822&quot; data-ke-size=&quot;size16&quot;&gt;그런데 우리 서비스에서 lob 데이터를 사용하고 있는게 있나 .. ? nop.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4210&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4212&quot; data-ke-size=&quot;size16&quot;&gt;우리 통계 배치 쿼리가 select insert 를 연산하면서 엄청 큰 데이터를 담을 공간이 임시로 보관한다면&amp;hellip;.??????&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3822&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://h2database.com/html/advanced.html#large_objects&quot;&gt;https://h2database.com/html/advanced.html#large_objects&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3822&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3822&quot; 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;스크린샷 2024-01-07 오후 8.03.21.png&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;151&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t6lJh/btsC4GRjzO9/FXrS490CO1WwFIaDgh28yK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t6lJh/btsC4GRjzO9/FXrS490CO1WwFIaDgh28yK/img.png&quot; data-alt=&quot;이정도면 그냥 의심병..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t6lJh/btsC4GRjzO9/FXrS490CO1WwFIaDgh28yK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft6lJh%2FbtsC4GRjzO9%2FFXrS490CO1WwFIaDgh28yK%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;259&quot; height=&quot;151&quot; data-filename=&quot;스크린샷 2024-01-07 오후 8.03.21.png&quot; data-origin-width=&quot;259&quot; data-origin-height=&quot;151&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이정도면 그냥 의심병..&lt;/figcaption&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;... pass&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;의심케이스-3---런타임에-설정한-cache-설정-반영되지-않음&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4303&quot; data-ke-size=&quot;size23&quot;&gt;의심케이스 4 - h2의 memory estimation 버그&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/h2database/h2database/pull/3626&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/h2database/h2database/pull/3626&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704625457072&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Improve memory estimation of CacheLongKeyLIRS and correct some passed memory sizes by katzyn &amp;middot; Pull Request #3626 &amp;middot; h2database&quot; data-og-description=&quot;Maximum size of CacheLongKeyLIRS now includes approximate size of the cache itself. It doesn't have any significant impact when large objects are cached, but it prevents excessive memory usage when...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/h2database/h2database/pull/3626&quot; data-og-url=&quot;https://github.com/h2database/h2database/pull/3626&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qsZfj/hyU2kIe7Xd/X2kFBiK2TKDkBVn2x4YHu1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/h2database/h2database/pull/3626&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/h2database/h2database/pull/3626&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qsZfj/hyU2kIe7Xd/X2kFBiK2TKDkBVn2x4YHu1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Improve memory estimation of CacheLongKeyLIRS and correct some passed memory sizes by katzyn &amp;middot; Pull Request #3626 &amp;middot; h2database&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Maximum size of CacheLongKeyLIRS now includes approximate size of the cache itself. It doesn't have any significant impact when large objects are cached, but it prevents excessive memory usage when...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2 의 구 버전들에서는 메모리 Estimation이 잘 못 되어, 캐시가 무한정 늘어나고 메모리 leak이 발생한다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 PR에서 해당 이슈가 해결되었고, 그냥 슥슥 훑어보니 int -&amp;gt; long 타입으로 변경하고, 여유 상수 값을 더하면서 더 정확하게 메모리 사용량을 측정하는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-01-07 오후 8.15.13.png&quot; data-origin-width=&quot;294&quot; data-origin-height=&quot;54&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wu3Yo/btsC7uikFwe/XBhommDcRRKB36x9lAHDV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wu3Yo/btsC7uikFwe/XBhommDcRRKB36x9lAHDV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wu3Yo/btsC7uikFwe/XBhommDcRRKB36x9lAHDV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwu3Yo%2FbtsC7uikFwe%2FXBhommDcRRKB36x9lAHDV1%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;294&quot; height=&quot;54&quot; data-filename=&quot;스크린샷 2024-01-07 오후 8.15.13.png&quot; data-origin-width=&quot;294&quot; data-origin-height=&quot;54&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전을 업그레이드 하면 문제가 해결될 수 있겠네요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;H2 커뮤니티에 직접 도움 요청하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/h2database/h2database/issues/3957&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/h2database/h2database/issues/3957&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704626283816&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;h2 1.4.200 Memory Leak by CacheLongKeyLIRS &amp;middot; Issue #3957 &amp;middot; h2database/h2database&quot; data-og-description=&quot;hi, we got memory leak problem of h2 1.4.200 version. may i get some advice? [env] spring boot 2.7 h2 1.4.2 Spring Data JPA ec2 docker container mem: 4gb jdbc:h2:file:/data/happy;CACHE_SIZE=2097152...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/h2database/h2database/issues/3957&quot; data-og-url=&quot;https://github.com/h2database/h2database/issues/3957&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/h9ryJ/hyUXPwl1Ok/ZMg8KlFa2MJNp8ENdKmNtk/img.png?width=1200&amp;amp;height=600&amp;amp;face=990_150_1044_210&quot;&gt;&lt;a href=&quot;https://github.com/h2database/h2database/issues/3957&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/h2database/h2database/issues/3957&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/h9ryJ/hyUXPwl1Ok/ZMg8KlFa2MJNp8ENdKmNtk/img.png?width=1200&amp;amp;height=600&amp;amp;face=990_150_1044_210');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;h2 1.4.200 Memory Leak by CacheLongKeyLIRS &amp;middot; Issue #3957 &amp;middot; h2database/h2database&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;hi, we got memory leak problem of h2 1.4.200 version. may i get some advice? [env] spring boot 2.7 h2 1.4.2 Spring Data JPA ec2 docker container mem: 4gb jdbc:h2:file:/data/happy;CACHE_SIZE=2097152...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 개발 서버 반영은 출근해야 가능하니, 못난 영어 실력으로 h2 에 이슈를 올려 소심하게 도움을 요청해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠르고 친절하게 알려주셔서 너무나도 감사했습니다. 이슈 해결되면 도네이션이라도 해야겠네용..&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(이미 시도) CACHE_SIZE 낮게 설정하기&lt;/li&gt;
&lt;li&gt;DB_CLOSE_ON_EXIT=FALSE AUTO_SERVER=TRUE 옵션 검토하기&lt;/li&gt;
&lt;li&gt;(위에서 언급) CACHE_SIZE 낮게 설정했더라도, 캐시 Estimation이 잘 못 되는 경우가 있어서 동일한 증상이 있었다고 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;H2-최신버전(2.2.224)으로-업그레이드-시도&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4607&quot; data-ke-size=&quot;size23&quot;&gt;H2 최신버전(2.2.224)으로 업그레이드 시도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 구 H2 버전에서 이러한 문제들이 있다고 하는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;h2 버전을 업데이트 하는 김에 설정을 다시 살펴봐야겠다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704626602482&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CACHE_SIZE=${spring.h2.cacheSize};

WRITE_DELAY=${spring.h2.writeDelay};

DB_CLOSE_DELAY=30;

DB_CLOSE_ON_EXIT=FALSE;

AUTO_SERVER=TRUE;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Combination of&lt;span&gt;&amp;nbsp;&lt;/span&gt;AUTO_SERVER=TRUE&lt;span&gt;&amp;nbsp;&lt;/span&gt;with&lt;span&gt;&amp;nbsp;&lt;/span&gt;DB_CLOSE_ON_EXIT=FALSE&lt;span&gt;&amp;nbsp;&lt;/span&gt;prevents correct database shutdown, so it can be a reason of some corruptions.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(This combination of settings is not allowed in modern versions of H2.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/h2database/h2database/commit/6a0f7a973251409f428565c7544dbd272de856f3&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/h2database/h2database/commit/6a0f7a973251409f428565c7544dbd272de856f3&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AUTO_SERVER=TRUE 와 DB_CLOSE_ON_EXIT=FALSE 옵션은 함께 사용하지 못한다고 합니다.&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;&lt;b&gt;AUTO_SERVER=TRUE&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;H2 기본 동작은 하나의 프로세스가 동작하고 있을때 다른 프로세스에서 접근할 수 없습니다.&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;그렇지만 저희 서버는 Active Active 상태로 로드밸런싱 되어서, 동일한 DB를 두 개의 서버가 참조하고 있었습니다.&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;&lt;b&gt;DB_CLOSE_ON_EXIT=FALSE&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5126&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5128&quot; data-ke-size=&quot;size16&quot;&gt;느낌상 이전에 해당 옵션을 넣은 이유는, 1번서버가 DB를 닫는 경우 2번 서버에서 DB 접근을 못하는 케이스가 있을 것 같아서 넣은 것으로 추측합니다&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5210&quot; 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;스크린샷 2024-01-07 오후 8.20.51.png&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGU00F/btsC7uo6vxS/OztlpRgPFEknfyBc2UdByk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGU00F/btsC7uo6vxS/OztlpRgPFEknfyBc2UdByk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGU00F/btsC7uo6vxS/OztlpRgPFEknfyBc2UdByk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGU00F%2FbtsC7uo6vxS%2FOztlpRgPFEknfyBc2UdByk%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;428&quot; height=&quot;263&quot; data-filename=&quot;스크린샷 2024-01-07 오후 8.20.51.png&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5210&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5212&quot; data-ke-size=&quot;size16&quot;&gt;그렇지만 위의 논의를 살펴보면 MSA 환경에서 H2에서 DB를 접근하는 경우, 왼쪽이 아닌 오른쪽과 같이 동작하는 것으로 보입니다. 그렇게되면 DB_CLOSE_ON_EXIT 에서 닫지 않는 프로세스는 본인의 프로세스이기 때문에 무관합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5212&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;DB_CLOSE_ON_EXIT=FALSE&lt;/b&gt; 옵션은 제거했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;H2 버전 업그레이드 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 누수 현상이 없어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 해당 현상은 잘못된 memory estimation으로 인해 h2의 캐시가 무한정 증식하는 버그였고, 최신 버전에서 패치가 된 것으로 보입니다.&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;h2 이전 이후에 많은 것이 바뀌어서 사이드이팩트를 조금 더 살펴볼 필요는 있을 것 같습니다.&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;캐시가 무한정 쌓이는 현상이라서 객체 생성 이후에 GC 가 일어나지 않는 일반적인 메모리 누수라고 볼 수는 없지만, 아무튼 heap dump를 통해 문제 원인을 파악하고 해결해나가는 과정이 너무나도 재미있었습니다. 덕분에 연관해서 많은 공부를 했고, 다음에 메모리 관련 이슈가 있을때 &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;주말이 없어졌네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 문서를 보고 데이터 1.0 버전의 데이터를 2.0 버전으로 옮기는 작업도 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.h2database.com/html/migration-to-v2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.h2database.com/html/migration-to-v2.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1704629500242&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Migration to 2.0&quot; data-og-description=&quot;Contents Introduction Upgrading File Format Data types Identity columns and sequences INFORMATION_SCHEMA General Introduction Between version 1.4.200 and version 2.0.202 there have been considerable changes, such that a simple update is not possible. It wo&quot; data-og-host=&quot;www.h2database.com&quot; data-og-source-url=&quot;https://www.h2database.com/html/migration-to-v2.html&quot; data-og-url=&quot;https://www.h2database.com/html/migration-to-v2.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.h2database.com/html/migration-to-v2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.h2database.com/html/migration-to-v2.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Migration to 2.0&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contents Introduction Upgrading File Format Data types Identity columns and sequences INFORMATION_SCHEMA General Introduction Between version 1.4.200 and version 2.0.202 there have been considerable changes, such that a simple update is not possible. It wo&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.h2database.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>깻묵(천진우)</author>
      <guid isPermaLink="true">https://netal.tistory.com/112</guid>
      <comments>https://netal.tistory.com/entry/heap-dump%EB%A1%9C-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%A6%AD-%EB%B6%84%EC%84%9D%ED%95%98%EA%B3%A0-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-H2-CacheLongKeyLIRS-memory-leak#entry112comment</comments>
      <pubDate>Sun, 7 Jan 2024 21:16:41 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 통계 테이블에서 데드락 해결하고 성능 개선하기</title>
      <link>https://netal.tistory.com/entry/DB-%ED%86%B5%EA%B3%84-%ED%85%8C%EC%9D%B4%EB%B8%94%EC%97%90%EC%84%9C-%EB%8D%B0%EB%93%9C%EB%9D%BD-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B3%A0-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
      <description>&lt;pre id=&quot;code_1703399690585&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;insert into B테이블
SELECT UNIX_TIMESTAMP(NOW()) -68, project_key ,segment_key, tenant_id ,
sum(active_user), sum(waiting_user), max(concurrent_user)

from A테이블 

where 
uts&amp;gt;= UNIX_TIMESTAMP(now()) -68 
and uts&amp;lt; UNIX_TIMESTAMP(now()) -8 

group by tenant_id, project_key, segment_key&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 기존에 H2 Database를 사용해서 실제 운영환경에 적용했습니다.&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;그러다가 새로운 서비스로 마이그레이션 하면서 MariaDB 를 활용하게 되었는데, 기존에 없었던 데드락 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-14 오후 5.08.11.png&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BFqRl/btsCxcXXemT/v75VXbGkM94yL4wM7CKhNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BFqRl/btsCxcXXemT/v75VXbGkM94yL4wM7CKhNk/img.png&quot; data-alt=&quot;무수한 데드락&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BFqRl/btsCxcXXemT/v75VXbGkM94yL4wM7CKhNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBFqRl%2FbtsCxcXXemT%2Fv75VXbGkM94yL4wM7CKhNk%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;1482&quot; height=&quot;190&quot; data-filename=&quot;스크린샷 2023-12-14 오후 5.08.11.png&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;무수한 데드락&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데드락 발생 시, 다음 쿼리를 통해 데드락에 대한 로그를 찾아볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(가장 최근의 데드락 로그만 담고 있습니다)&lt;/p&gt;
&lt;pre id=&quot;code_1703396560315&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;show engine innodb status&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;/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_1703396435618&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INSERT INTO A테이블 (~~~) VALUES (~~~)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1703396491602&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;insert into B테이블
SELECT UNIX_TIMESTAMP(NOW()) -68, project_key ,segment_key, tenant_id ,
sum(active_user), sum(waiting_user), max(concurrent_user)

from A테이블 

where 
uts&amp;gt;= UNIX_TIMESTAMP(now()) -68 
and uts&amp;lt; UNIX_TIMESTAMP(now()) -8 

group by tenant_id, project_key, segment_key&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;원인을 생각해보면, MairaDB의 기본 격리수준인 REPEATABLE_READ 에서 insert ~ select 쿼리를 사용하게 되면서 Source Table 이 잠금이 일어나서 발생하는 문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 잠금 범위를 최소화하고 실행 시간을 최소화하면 데드락이 줄어들지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능개선 Step1. Bulk Insert 적용으로 실행시간을 줄여보기&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-12-06 오후 10.26.29.png&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyeLgx/btsCy5DCU14/WKiJqWrOYcHnekfjq0RZmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyeLgx/btsCy5DCU14/WKiJqWrOYcHnekfjq0RZmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyeLgx/btsCy5DCU14/WKiJqWrOYcHnekfjq0RZmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyeLgx%2FbtsCy5DCU14%2FWKiJqWrOYcHnekfjq0RZmk%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;524&quot; height=&quot;358&quot; data-filename=&quot;edited_스크린샷 2023-12-06 오후 10.26.29.png&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무려 한 번의 요청에 평균 56번의 쿼리가 나가고 있고, 해당 쿼리가 무엇인지를 보여주고 있습니다.&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;왜 평균 56번이나 되는 쿼리가 나갔을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직을 살펴봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1703397361653&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public void putLogData(List&amp;lt;PutNFDataModel&amp;gt; dataModels) {
    final Long now = TimeUtil.getNowTs();
    List&amp;lt;NFStat&amp;gt; nfStats dataModels.stream()
            .map(dataModel -&amp;gt; dataModel.toNFStat(now))
            .toList();
    nfStatsRepository.saveAll(nfStats);
}&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;실제로 요청에 들어오는 데이터를 보면 한 번에 5~30개의 데이터가 들어오게 되는데, 해당 쿼리를 saveAll을 통해 호출하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;saveAll 기능은 내부에서 save를 반복문으로 호출하는 기능이기 때문에 N개의 데이터 당 N개의 쿼리가 발생하게 됩니다.&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;심지어 PK가 AutoIncrement 가 아니기 때문에 select 이후에 insert 가 나가서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2N 개의 쿼리가 나가고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[위 내용이 자세하게 설명되어 있는 우리 승현쌤의 블로그]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://imksh.com/113&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://imksh.com/113&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1703397531479&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;JPA saveAll이 Bulk INSERT 되지 않았던 이유&quot; data-og-description=&quot;실습 환경 MySQL 5.7버전 사용 Windows 10 Entity ID 전략은 IDENTITY Java 11 서론 실무에서 MySQL 5.7 버전을 사용하고 있고, JPA Entity의 ID 전략은 IDENTITY를 사용하고 있는데, 이때 JPARepository를 이용한 saveAll과 sa&quot; data-og-host=&quot;imksh.com&quot; data-og-source-url=&quot;https://imksh.com/113&quot; data-og-url=&quot;https://imksh.com/113&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bQ8KFV/hyUTJumPEp/giUCjF7THT6pAIKq64N9fK/img.png?width=670&amp;amp;height=660&amp;amp;face=0_0_670_660,https://scrap.kakaocdn.net/dn/btKZwA/hyUTyGohLP/8F8vyvGbZJ1Jcba7jNuuik/img.png?width=670&amp;amp;height=660&amp;amp;face=0_0_670_660,https://scrap.kakaocdn.net/dn/bDXpyS/hyUTI95Cqk/CveaVt5KpGWjTfc6wk4DIk/img.png?width=1920&amp;amp;height=989&amp;amp;face=0_0_1920_989&quot;&gt;&lt;a href=&quot;https://imksh.com/113&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://imksh.com/113&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bQ8KFV/hyUTJumPEp/giUCjF7THT6pAIKq64N9fK/img.png?width=670&amp;amp;height=660&amp;amp;face=0_0_670_660,https://scrap.kakaocdn.net/dn/btKZwA/hyUTyGohLP/8F8vyvGbZJ1Jcba7jNuuik/img.png?width=670&amp;amp;height=660&amp;amp;face=0_0_670_660,https://scrap.kakaocdn.net/dn/bDXpyS/hyUTI95Cqk/CveaVt5KpGWjTfc6wk4DIk/img.png?width=1920&amp;amp;height=989&amp;amp;face=0_0_1920_989');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;JPA saveAll이 Bulk INSERT 되지 않았던 이유&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;실습 환경 MySQL 5.7버전 사용 Windows 10 Entity ID 전략은 IDENTITY Java 11 서론 실무에서 MySQL 5.7 버전을 사용하고 있고, JPA Entity의 ID 전략은 IDENTITY를 사용하고 있는데, 이때 JPARepository를 이용한 saveAll과 sa&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;imksh.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;하나의 batch insert 쿼리만 나갈 수 있도록 jdbcTemplate.batchUpdate를 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1703397763064&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Override
    public void saveAllBulk(List&amp;lt;NFStat&amp;gt; nfStats) {
        String sql = &quot;INSERT INTO nf_stat (&quot;timestamp, tenant_id, nfid, ...)
                &quot;VALUES (&quot; +
                &quot;?, ?, ?, ?, ?, &quot; +
                &quot;?, ?, ?, ?, ?, &quot; +
                &quot;?, ?, ?, ?, ?, &quot; +
                &quot;?, ?, ?, ?, ?, &quot; +
                &quot;?, ?, ?, ?, ?, &quot; +
                &quot;?, ?, ?, ?, ?, &quot; +
                &quot;?, ?, ?, ?, ?, &quot; +
                &quot;?, ?, ?, ?, ?, &quot; +
                &quot;?, ?, ?, ?, ?, &quot; +
                &quot;?, ?, ?, ?, ?, &quot; +
                &quot;?, ?, ?, ?)&quot;;

        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {
                NFStat nfStat = nfStats.get(i);
                preparedStatement.setLong(1, nfStat.getTimeStamp());
                preparedStatement.setString(2, nfStat.getTenantId());
                ...
                ...
                preparedStatement.setInt(53, nfStat.getDynamicVarMax());
                preparedStatement.setInt(54, nfStat.getDynamicIdleTps());
            }

            @Override
            public int getBatchSize() {
                return nfStats.size();
            }
        });
    }&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;batch insert를 하게 되면 네트워크 비용이 감소하게 되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 h2 환경에서는 모르겠지만.. 최신 RDBMS 옵티마이저는 bulk insert 최적화가 굉장히 잘되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_bulk_insert_buffer_size&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_bulk_insert_buffer_size&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1703397880022&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;MySQL :: MySQL 8.0 Reference Manual :: 5.1.8 Server System Variables&quot; data-og-description=&quot;&quot; data-og-host=&quot;dev.mysql.com&quot; data-og-source-url=&quot;https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_bulk_insert_buffer_size&quot; data-og-url=&quot;https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_bulk_insert_buffer_size&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_bulk_insert_buffer_size&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_bulk_insert_buffer_size&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MySQL :: MySQL 8.0 Reference Manual :: 5.1.8 Server System Variables&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev.mysql.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능개선 Step2. 인덱스 강제로 태우기 (force index)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL이나 MariaDB는 실제 row 에 잠금을 하는 것이 아닌 인덱스를 대상으로 잠금을 하기 때문에, 인덱스가 최소 범위로 걸리는 것은 매우 중요합니다.&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_1703399698072&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;insert into B테이블
SELECT UNIX_TIMESTAMP(NOW()) -68, project_key ,segment_key, tenant_id ,
sum(active_user), sum(waiting_user), max(concurrent_user)

from A테이블 

where 
uts&amp;gt;= UNIX_TIMESTAMP(now()) -68 
and uts&amp;lt; UNIX_TIMESTAMP(now()) -8 

group by tenant_id, project_key, segment_key&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;/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;where 절에 너무 많은 함수들이 들어가있고, 옵티마이저의 판단을 흐리게하는 조건문이라고 생각했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 explain으로 조회해보니..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-12-24 오후 3.33.44.png&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p9XOr/btsCCwNPGW9/Ke6iy9ideQQxHWsc05H27k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p9XOr/btsCCwNPGW9/Ke6iy9ideQQxHWsc05H27k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p9XOr/btsCCwNPGW9/Ke6iy9ideQQxHWsc05H27k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp9XOr%2FbtsCCwNPGW9%2FKe6iy9ideQQxHWsc05H27k%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;1562&quot; height=&quot;344&quot; data-filename=&quot;edited_스크린샷 2023-12-24 오후 3.33.44.png&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;344&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;실제로 Possible Key 에는 uts 컬럼에 설정한 인덱스를 후보로 올렸지만, 실질적으로는 풀테이블 스캔을 한 것을 볼 수 있습니다.&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; 그래서 다음과 같이 강제로 인덱스를 태워서 쿼리를 실행하도록 force index 실행 힌트를 제공했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1703399831886&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;insert into B테이블
SELECT UNIX_TIMESTAMP(NOW()) -68, project_key ,segment_key, tenant_id ,
sum(active_user), sum(waiting_user), max(concurrent_user)

from nf_stat force index (idx_nf_stat_uts)

where 
uts&amp;gt;= UNIX_TIMESTAMP(now()) -68 
and uts&amp;lt; UNIX_TIMESTAMP(now()) -8 

group by tenant_id, project_key, segment_key&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;성공적으로 range scan을 하게 되고, 실제로 탐색한 row 도 줄었는 모습을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qZ3ER/btsCyv3Dg59/5fC3ZoKBav48c6kpJFayy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qZ3ER/btsCyv3Dg59/5fC3ZoKBav48c6kpJFayy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qZ3ER/btsCyv3Dg59/5fC3ZoKBav48c6kpJFayy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqZ3ER%2FbtsCyv3Dg59%2F5fC3ZoKBav48c6kpJFayy1%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;1572&quot; height=&quot;260&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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;/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;b&gt;스프링 서버의 latency 입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-12-14 오후 4.50.56.png&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAJ4An/btsDdJmBGE9/ywZfLHLYELJ8zBRUAup5Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAJ4An/btsDdJmBGE9/ywZfLHLYELJ8zBRUAup5Qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAJ4An/btsDdJmBGE9/ywZfLHLYELJ8zBRUAup5Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAJ4An%2FbtsDdJmBGE9%2FywZfLHLYELJ8zBRUAup5Qk%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;1670&quot; height=&quot;510&quot; data-filename=&quot;edited_스크린샷 2023-12-14 오후 4.50.56.png&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MariaDB의 &lt;b&gt;latency&lt;/b&gt; / 요청수를 볼까요?&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_KakaoTalk_Photo_2024-01-10-07-33-50.png&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSO5CH/btsDhgjRCQr/sJIZ1R2KPRwpCgoNbeEFy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSO5CH/btsDhgjRCQr/sJIZ1R2KPRwpCgoNbeEFy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSO5CH/btsDhgjRCQr/sJIZ1R2KPRwpCgoNbeEFy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSO5CH%2FbtsDhgjRCQr%2FsJIZ1R2KPRwpCgoNbeEFy1%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;1786&quot; height=&quot;528&quot; data-filename=&quot;edited_KakaoTalk_Photo_2024-01-10-07-33-50.png&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cG96AC/btsDbd9v7EW/ARbWkObNW0n3nNOtQPc7F0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cG96AC/btsDbd9v7EW/ARbWkObNW0n3nNOtQPc7F0/img.png&quot; data-alt=&quot;SRE 엔지니어와 팀리드님의 칭찬  &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cG96AC/btsDbd9v7EW/ARbWkObNW0n3nNOtQPc7F0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcG96AC%2FbtsDbd9v7EW%2FARbWkObNW0n3nNOtQPc7F0%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;342&quot; height=&quot;322&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SRE 엔지니어와 팀리드님의 칭찬  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세션 격리수준 Read Committed 적용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 데드락 빈도가 줄었을 뿐 여전히 간혈적으로 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고객사당 3초에 한 번씩 요청에 보내고 있었고, SaaS서비스 오픈 이후 고객사들이 늘어날수록 더 많은 데드락이 발생할 수 밖에 없는 구조였습니다.&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;그렇지만 격리 수준 낮추는 것은 데이터베이스 단위로 영향을 받기 때문에 신중해야하고, 입사한지 얼마 되지 않아서 히스토리를 모르기에 해당 세션만 격리 수준을 낮추는 것으로 판단했습니다.&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;해당 select insert 쿼리가 나가는 세션에 해당 옵션을 주어서 문제를 해결했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1703398705356&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITEED;&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;h2 data-ke-size=&quot;size26&quot;&gt;데드락 해결 결과&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-14 오후 5.02.08.png&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpx3qC/btsCybqO7B5/qUZtvK8gFVrTnJerYOJLb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpx3qC/btsCybqO7B5/qUZtvK8gFVrTnJerYOJLb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpx3qC/btsCybqO7B5/qUZtvK8gFVrTnJerYOJLb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpx3qC%2FbtsCybqO7B5%2FqUZtvK8gFVrTnJerYOJLb1%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;1670&quot; height=&quot;524&quot; data-filename=&quot;스크린샷 2023-12-14 오후 5.02.08.png&quot; data-origin-width=&quot;1670&quot; data-origin-height=&quot;524&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h2 data-ke-size=&quot;size26&quot;&gt;REF&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hoing.io/archives/8067&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hoing.io/archives/8067&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1703398738658&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;MySQL CTAS/Insert Select 에서 공유락(Shared Lock) 문제 와 발생 원인 이유&quot; data-og-description=&quot; &quot; data-og-host=&quot;hoing.io&quot; data-og-source-url=&quot;https://hoing.io/archives/8067&quot; data-og-url=&quot;https://hoing.io/archives/8067&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/coshfG/hyUPL1HBNg/qggbzUvbkJnnCYn8KThhs0/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/HKEC0/hyUPCwVJu5/8JFz9XesfkIvLe79qjuBm1/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/cdp27Y/hyUPHE0IuN/1UjwdnMKX6O7NpHB2G2Krk/img.png?width=759&amp;amp;height=500&amp;amp;face=0_0_759_500&quot;&gt;&lt;a href=&quot;https://hoing.io/archives/8067&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hoing.io/archives/8067&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/coshfG/hyUPL1HBNg/qggbzUvbkJnnCYn8KThhs0/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/HKEC0/hyUPCwVJu5/8JFz9XesfkIvLe79qjuBm1/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/cdp27Y/hyUPHE0IuN/1UjwdnMKX6O7NpHB2G2Krk/img.png?width=759&amp;amp;height=500&amp;amp;face=0_0_759_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MySQL CTAS/Insert Select 에서 공유락(Shared Lock) 문제 와 발생 원인 이유&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hoing.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB</category>
      <author>깻묵(천진우)</author>
      <guid isPermaLink="true">https://netal.tistory.com/110</guid>
      <comments>https://netal.tistory.com/entry/DB-%ED%86%B5%EA%B3%84-%ED%85%8C%EC%9D%B4%EB%B8%94%EC%97%90%EC%84%9C-%EB%8D%B0%EB%93%9C%EB%9D%BD-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B3%A0-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0#entry110comment</comments>
      <pubDate>Sun, 24 Dec 2023 15:20:00 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 트랜잭션(Transaction) 우아하게  분리하기</title>
      <link>https://netal.tistory.com/entry/Spring-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98Transaction-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 단위는 일반적으로 짧게 가져가고 것이 이상적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유에 대해서 살펴보고, 어떻게 구성해야 하는지에 대해서 고민해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Transactional 남용의 문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional 어노테이션은 너무나도 편리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 트랜잭션의 시작과 종료를 코드로 명시하지 않아도, Spring AOP를 통해 간단하게 어노테이션으로 트랜잭션 범위를 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통적인 횡단 관심사를 해결하는 Spring AOP의 대표적인 사례라고 볼 수 있겠습니다.&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;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;@Transactional // 적절한 트랜잭션인가?
public void processPost(Long userId, String content, MultipartFile image) {
    // 유저 조회 - select
    User user = userRepository.getById(userId);

    // 게시글 생성 - insert
    Post post = new Post();
    postRepository.save(post);

    // 게시글 이미지 업로드
    s3Uploader.upload(image);

    // 해당 유저 구독자들에게 푸시 알람 전송
    notifyService.notify(userId, postId, reportCount);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 고민거리가 생기게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게시글 이미지가 100MB라면 어떻게 될까요?&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;h2 data-ke-size=&quot;size26&quot;&gt;Item. 트랜잭션을 짧게 가져가자 - undo log&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 짧게 가져가야 하는 이유는 MVCC 의 내부 구현 때문에 그렇습니다.&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;Oracle이나 MySQL 등 RDBMS를 보면, MVCC 구현을 위해서&amp;nbsp; &lt;u&gt;&lt;b&gt;Undo Log&lt;/b&gt;&lt;/u&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;1662&quot; data-origin-height=&quot;1282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKkDzd/btsviSSQ05y/eHQOxqRikyEMWXi5zywHD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKkDzd/btsviSSQ05y/eHQOxqRikyEMWXi5zywHD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKkDzd/btsviSSQ05y/eHQOxqRikyEMWXi5zywHD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKkDzd%2FbtsviSSQ05y%2FeHQOxqRikyEMWXi5zywHD0%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;580&quot; height=&quot;447&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;1282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언두 로그는 트랜잭션의 롤백을 지원하고, 트랜잭션끼리의 격리를 위해 설계된 매커니즘입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 읽기를 위한 SELECT 절의 경우 트랜잭션으로 인한 Lock을 기다리지 않고, &lt;u&gt;&lt;b&gt;잠금 없는 일관된 읽기&lt;/b&gt;&lt;/u&gt;를 가능하게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 언두 로그는 해당 인덱스(컬럼)를 기준으로 트랜잭션이 중첩되면 로그 또한 중첩되서 쌓이게 되며, 해당 컬럼에 대해서 &lt;b&gt;아무 트랜잭션이 없을 때까지 정리되지 않습니다&lt;/b&gt;. 그래서 트랜잭션을 오래 물고 있는 경우 다음과 같은 상태가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-09-24 오후 1.02.41.png&quot; data-origin-width=&quot;1576&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFJP3z/btsvvFKXGYt/uylQCqttFWXvRekYtXUuvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFJP3z/btsvvFKXGYt/uylQCqttFWXvRekYtXUuvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFJP3z/btsvvFKXGYt/uylQCqttFWXvRekYtXUuvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFJP3z%2FbtsvvFKXGYt%2FuylQCqttFWXvRekYtXUuvk%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;533&quot; height=&quot;231&quot; data-filename=&quot;edited_스크린샷 2023-09-24 오후 1.02.41.png&quot; data-origin-width=&quot;1576&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언두 로그가 과도하게 쌓이게 되면 잠금 없는 일관된 읽기를 위해 &lt;b&gt;탐색 비용이 발생해 읽기 성능이 떨어지고&lt;/b&gt;, 또한 만약 다른 트랜잭션들이 동일한 자원을 필요로 하는 경우 &lt;b&gt;데드락이 발생할 가능성이 높아집니다&lt;/b&gt;.&lt;/p&gt;
&lt;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;h2 data-ke-size=&quot;size26&quot;&gt;Item. 외부 연결은 트랜잭션에서 제외하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션의 길이를 짧게 가져가야 하는 것에 대한 필요성을 알았다면, 왜 외부 연결을 트랜잭션에서 제외해야 하는지에 대해 알게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드를 문제와 함께 다시 살펴봅시다.&lt;/p&gt;
&lt;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;// 게시글 이미지 업로드 - 이미지 업로드가 10초 걸린다면?
s3Uploader.upload(image);

// 해당 유저 구독자들에게 푸시 알람 전송 - 푸시 서버가 고장난다면?
notifyService.notify(userId, postId, reportCount);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 문제가 발생했다고 가정하고, 트랜잭션이 어떻게 될 지 생각해볼까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-09-24 오후 1.37.31.png&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFOiaG/btsvk1aKzy5/rVQpE5Ss1xyxCFJgVu91xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFOiaG/btsvk1aKzy5/rVQpE5Ss1xyxCFJgVu91xK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFOiaG/btsvk1aKzy5/rVQpE5Ss1xyxCFJgVu91xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFOiaG%2Fbtsvk1aKzy5%2FrVQpE5Ss1xyxCFJgVu91xK%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;442&quot; height=&quot;320&quot; data-filename=&quot;edited_스크린샷 2023-09-24 오후 1.37.31.png&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;1058&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;p data-ke-size=&quot;size16&quot;&gt;만약 게시글 작성이 몇 백개가 일어난다면, DB 뿐만 아니라 서블릿의 커넥션 풀도 고갈되면서 서버는 바로 먹통이 될 것이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 해당 DB를 참조하는 다른 서비스들도 같이 장애가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;그렇다면 간단하게 매서드로 빼내서 @Transactional을 붙여주는 건 어떨까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드를 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;public void processPost(Long userId, String content, MultipartFile image) {

    // 게시글 이미지 업로드 - 이미지 업로드가 10초 걸린다면?
    s3Uploader.upload(image);

    uploadPostTransaction(userId);
            
    // 해당 유저 구독자들에게 푸시 알람 전송 - 푸시 서버가 고장난다면?
    notifyService.notify(userId, postId, reportCount);
}

@Transactional // 가능한가?
private void uploadPostTransaction(Long userId) {
    // 유저 조회 - select
    User user = userRepository.getById(userId);

    // 게시글 생성 - insert
    Post post = new Post(user);
    postRepository.save(post);
}&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;첫 번째로 스프링 AOP 는 public 매서드에만 동작합니다. 결국 프록시 패턴으로 인해 외부에서 해당 매서드를 호출해야 하기 때문입니다.&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;그러면 접근제어자로 인한 캡슐화를 포기하고, public을 사용하면 트랜잭션이 동작할까요?&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public void uploadPostTransaction(Long userId) {&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;logging.level.org.hibernate.engine.transaction.internal.TransactionImpl=debug&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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 void processPost() {
    System.out.println(&quot;트랜잭션 시작 전&quot;);
    uploadPostTransaction();
    System.out.println(&quot;트랜잭션 종료 후&quot;);
}

@Transactional // 트랜잭션이 열릴까?
public void uploadPostTransaction() {
    System.out.println(&quot;트랜잭션 로직 시작&quot;);
    System.out.println(&quot;트랜잭션 로직 종료&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-24 오후 2.35.08.png&quot; data-origin-width=&quot;376&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kTfAZ/btsvkRlAo3R/HoK56ZGdjkVuiVJ2AA6jcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kTfAZ/btsvkRlAo3R/HoK56ZGdjkVuiVJ2AA6jcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kTfAZ/btsvkRlAo3R/HoK56ZGdjkVuiVJ2AA6jcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkTfAZ%2FbtsvkRlAo3R%2FHoK56ZGdjkVuiVJ2AA6jcK%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;283&quot; height=&quot;224&quot; data-filename=&quot;스크린샷 2023-09-24 오후 2.35.08.png&quot; data-origin-width=&quot;376&quot; data-origin-height=&quot;298&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;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;동일한 Bean&lt;span&gt;&amp;nbsp;&lt;/span&gt;내에서 순수한 함수가 @Transactional이 선언된 public 메서드를 호출해도, 트랜잭션은 적용되지 않습니다.&lt;/b&gt;&lt;/u&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;스프링 AOP 의 내부 동작을 본다면 이유를 찾아볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-09-25 오후 2.52.48.png&quot; data-origin-width=&quot;2052&quot; data-origin-height=&quot;1354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJXg4W/btsvlzMnEdo/fJkMDlJJBP2HQ1UaepjeV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJXg4W/btsvlzMnEdo/fJkMDlJJBP2HQ1UaepjeV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJXg4W/btsvlzMnEdo/fJkMDlJJBP2HQ1UaepjeV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJXg4W%2FbtsvlzMnEdo%2FfJkMDlJJBP2HQ1UaepjeV1%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;692&quot; height=&quot;457&quot; data-filename=&quot;edited_스크린샷 2023-09-25 오후 2.52.48.png&quot; data-origin-width=&quot;2052&quot; data-origin-height=&quot;1354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 호출이 original 객체의 proccessPost 매서드를 호출하게 되고, 해당 매서드 내부에서 uploadPost를 호출하게 되면 같은 객체를 내부에서&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;h2 data-ke-size=&quot;size26&quot;&gt;트랜잭션 분리하기 - 별도의 객체(빈)으로 분리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 위의 문제를 해결하는 방법 중 하나는 별도의 객체로 분리하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어찌 보면 당연하지만,&lt;/p&gt;
&lt;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;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 void processPost(Long userId, String content, MultipartFile image) {
        // 게시글 이미지 업로드 - 이미지 업로드가 10초 걸린다면?
        s3Uploader.upload(image);

        // 포스트 생성
        postCommand.postCreateCommand(userId, content);

        // 해당 유저 구독자들에게 푸시 알람 전송 - 푸시 서버가 고장난다면?
        notifyService.notify(userId, postId, reportCount);
    }
}

//별개의 클래스(빈)으로 분리
@Component
class PostCommand() {

	@Transactional
    public void postCreateCommand(Long userId) {
        // 유저 조회 - select
        User user = userRepository.getById(userId);

        // 게시글 생성 - insert
        Post post = new Post(user);
        postRepository.save(post);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;트랜잭션 분리하기 - 트랜잭션이 불필요한 클래스를 비동기로 처리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 클래스로 분리하게 되면 비동기로 처리하는 방식도 선택지가 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 동기로 처리해야 하거나, 해당 로직을 트랜잭션에 포함해야 하는 경우에는 사용하지 못하겠죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;@Async
public void notify(Long postId, Long userId, Long reportCount) {
    slackPostReportFeignClient.call(test);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트랜잭션 분리하기 - TransactionTemplate 활용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 에서는 TransactionTemplate을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따로 Transactional을 사용하지 않고도 트랜잭션을 사용할 수 있으며, JDBC template 에서 직접 트랜잭션을 열고 닫는 것의 차선책이라고 볼 수 있을 것 같습니다.&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;그렇지만 실제로 몇 번 경험해보니, mock을 통한 유닛 테스트가 쉽지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transactionTemplate을 mocking 하게 되면 transaction에 묶인 내부 로직들이 테스트 하기 힘들어지는데, 다음에 한 번 더 생각을 해봐야겠네요.&lt;/p&gt;
&lt;p 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;reasonml&quot;&gt;&lt;code&gt;public void processPost(Long userId, String content, MultipartFile image) {
    // 게시글 이미지 업로드 - 이미지 업로드가 10초 걸린다면?
    s3Uploader.upload(image);

    // 게시글 생성 - insert
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            User user = userRepository.getById(userId);
            Post post = new Post(user);
            postRepository.save(post);
        }
    });

    // 해당 유저 구독자들에게 푸시 알람 전송 - 푸시 서버가 고장난다면?
    notifyService.notify(userId, postId, reportCount);
}&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&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;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 분리해야 하는 이유와, 어떻게 분리하는 게 좋을지를 알아봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 스프링 AOP의 특성 때문에 주의해야 하는 케이스까지 알아봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 또 걱정되는 것 중에 하나는, '&lt;b&gt;이른 최적화'&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 서비스에서는 위의 문제로 인해 장애가 발생할 확률이 낮고, 첫 개발부터 위의 케이스들을 모두 고려해서 TransactionTemplate 등을 넣는다던가 클래스를 분리한다면 코드를 작성하는 비용이 꽤나 커지게 될 것 같습니다.&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;일단 무조건 @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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://altasumut.medium.com/spring-aop-q-a-b91a165d50f5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://altasumut.medium.com/spring-aop-q-a-b91a165d50f5&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>깻묵(천진우)</author>
      <guid isPermaLink="true">https://netal.tistory.com/109</guid>
      <comments>https://netal.tistory.com/entry/Spring-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98Transaction-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0#entry109comment</comments>
      <pubDate>Sun, 24 Sep 2023 15:18:50 +0900</pubDate>
    </item>
    <item>
      <title>[프로젝트] 사이드 프로젝트에서 발생한 매몰비용</title>
      <link>https://netal.tistory.com/entry/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EB%A7%A4%EB%AA%B0%EB%B9%84%EC%9A%A9</link>
      <description>&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;스크린샷 2023-09-20 오후 4.56.53.png&quot; data-origin-width=&quot;566&quot; data-origin-height=&quot;165&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckeq4M/btsuJgNkrSx/zfuYVZJgdHzsSAMpRRTqrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckeq4M/btsuJgNkrSx/zfuYVZJgdHzsSAMpRRTqrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckeq4M/btsuJgNkrSx/zfuYVZJgdHzsSAMpRRTqrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fckeq4M%2FbtsuJgNkrSx%2FzfuYVZJgdHzsSAMpRRTqrk%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;600&quot; height=&quot;175&quot; data-filename=&quot;스크린샷 2023-09-20 오후 4.56.53.png&quot; data-origin-width=&quot;566&quot; data-origin-height=&quot;165&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 일주일 동안 주변 사람들과 '사이드 프로젝트의 매몰 비용'에 대한 이야기가 오고 갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 년도에 여러개의 토이 프로젝트를 진행하면서 다양한 이유로 놓지 못하고 있었던 프로젝트들이 많았고, 이런저런 생각들을 공유해야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;#1 포착 - 1월동안 9월까지 진행한 프로젝트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.youtube.com/watch?v=dE2L2Us77so&amp;amp;ab_channel=OGAMEntertainment&quot;&gt;(015B - 1월부터 6월까지)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;1. 현실적으로 만들 수 있는 규모인가 ?&lt;br /&gt;2. 모두가 일정 시간 이상의 시간을 투자할 수 있는가 ?&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;우리 동아리는 2개월 만에 빠르게 런칭하는 프로젝트였다. 그렇지만 초기 기획의 Feature가 너무 비대했던 탓에, 제시간에 런칭하지 못해서 장기 프로젝트가 되었다.&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;이후에 '동아리'라는 강제 요소가 없이 직장인 분들과 함께 프로젝트를 진행하다 보니, 한계점이 명확해졌다. 이 후에 투자할 수 있는 시간이 각각 달랐고, 맴버 한 둘이 개인 일정으로 뜸하게 되면 필수적인 maintainer들도 없어진다 ..&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;1006&quot; data-origin-height=&quot;976&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nVFSi/btsuOfOl8pe/CNYA93RLpqK7Xhzg4F3rAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nVFSi/btsuOfOl8pe/CNYA93RLpqK7Xhzg4F3rAK/img.png&quot; data-alt=&quot;github의 2 month ago를 보고, 카톡에서 팀원 협박 중.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nVFSi/btsuOfOl8pe/CNYA93RLpqK7Xhzg4F3rAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnVFSi%2FbtsuOfOl8pe%2FCNYA93RLpqK7Xhzg4F3rAK%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;327&quot; height=&quot;317&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;976&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;github의 2 month ago를 보고, 카톡에서 팀원 협박 중.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;3. 팀원들이 프로젝트서 추구하는 목표가 같은가 ?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로젝트의 시작부터 끝까지 '목표'에 대해서 얘기하고 동의를 이끌어낸 적이 없었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 개개인의 목표만이 존재했고, 끝까지 공유되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 토이 프로젝트를 하는 목표는 크게 다음으로 나뉜다.&lt;/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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;이후에 생각해 보니 팀원들은 각자 너무나도 다양한 목표를 가지고 있었고, 목표가 컸던 사람들일수록 유지하고자 하는 마음에 매몰비용이 커져갔다.&lt;/b&gt;&lt;/u&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;끝내 안드로이드, iOS 모두 스토어에 런칭했고, 백엔드는 즐겁게 리팩토링을 했으니 행복한 결말을 맞이했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 9명 다 같이 모여서 보드게임을 하고, 팀원 한 분의 기쁜 임신 소식을 전해 들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-20 오후 5.31.55.png&quot; data-origin-width=&quot;227&quot; data-origin-height=&quot;85&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mOKjz/btsuJbL3tL5/wBldvxr9gbuGmqcezQFXm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mOKjz/btsuJbL3tL5/wBldvxr9gbuGmqcezQFXm1/img.png&quot; data-alt=&quot;삐- 사망선고&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mOKjz/btsuJbL3tL5/wBldvxr9gbuGmqcezQFXm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmOKjz%2FbtsuJbL3tL5%2FwBldvxr9gbuGmqcezQFXm1%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;227&quot; height=&quot;85&quot; data-filename=&quot;스크린샷 2023-09-20 오후 5.31.55.png&quot; data-origin-width=&quot;227&quot; data-origin-height=&quot;85&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;삐- 사망선고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;#2 짠지 - 7월부터 9월까지 진행한 프로젝트&lt;/h2&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&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5ifP4/btsuPPhBAZZ/IFXo9hzuoBalx75Vqhviwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5ifP4/btsuPPhBAZZ/IFXo9hzuoBalx75Vqhviwk/img.png&quot; style=&quot;width: 35.20363687784126%;&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;193&quot; data-filename=&quot;스크린샷 2023-09-20 오후 5.08.10.png&quot; data-widthpercent=&quot;36.04&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5ifP4/btsuPPhBAZZ/IFXo9hzuoBalx75Vqhviwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5ifP4%2FbtsuPPhBAZZ%2FIFXo9hzuoBalx75Vqhviwk%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;360&quot; height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doQ477/btsuZ1nChaT/pSYZgaY3wIOpMpiHUYzvC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doQ477/btsuZ1nChaT/pSYZgaY3wIOpMpiHUYzvC1/img.png&quot; style=&quot;width: 30.57265065909457%;&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;221&quot; data-filename=&quot;스크린샷 2023-09-20 오후 5.08.19.png&quot; data-widthpercent=&quot;31.3&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doQ477/btsuZ1nChaT/pSYZgaY3wIOpMpiHUYzvC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoQ477%2FbtsuZ1nChaT%2FpSYZgaY3wIOpMpiHUYzvC1%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;358&quot; height=&quot;221&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blocIN/btsuOgNefTr/40lYeL1E0vAWxNRCTQ7k8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blocIN/btsuOgNefTr/40lYeL1E0vAWxNRCTQ7k8k/img.png&quot; style=&quot;width: 31.89813106771532%;&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;213&quot; data-filename=&quot;스크린샷 2023-09-20 오후 5.08.27.png&quot; data-widthpercent=&quot;32.66&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blocIN/btsuOgNefTr/40lYeL1E0vAWxNRCTQ7k8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblocIN%2FbtsuOgNefTr%2F40lYeL1E0vAWxNRCTQ7k8k%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;360&quot; height=&quot;213&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오! 이번에는 2개월만에 후다닥 MVP를 만들고, 팀원들과의 상호 협의로 프로젝트를 폭파시키는 것에 성공했다 (?!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그렇다면 끝내자마자 바로 폐기했으니 매몰 비용이 생기지 않았을까? 일부 팀원들과 나의 생각은 그렇지 않았다. 이 프로젝트에 투자한 2개월 조차 매몰 비용으로 들어갔다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;4. 정말 '내가' 쓰고 싶은 서비스인가?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짠지는 소비 내용을 기록하는 것이 main feature다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 요즘 금융 서비스들은 자동 소비 내역 저장을 너무나도 잘 해주고 있고, 이것을 실제 유저가 입력하는 것은 너무나도 큰 장애물이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 2개월간 서비스를 기획하고 개발한 우리도 쓰지 않는 서비스를 만들어버렸고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;프로젝트에 대한 애정이 없어졌다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maintainer에게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;프로젝트는, 나를 위한 서비스와 고객을 위한 서비스로 나뉜다.&lt;/b&gt;&lt;/u&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;u&gt;&lt;b&gt;그렇지만 적어도 사이드 프로젝트에서는 내가 쓰고 싶은 서비스를 만들어야 한다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 기획대로 잘 만들어지더라도, 토이프로젝트에서는 실제 고객을 끌어낼 자금도 없고 가능성이 적다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무런 보상도 없이 작은 확률에 기대하며, 내가 쓰지 않을 프로젝트에 길고 긴 시간을 투자하는 것은 당연히 너무나도 지루한 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(사실 실제 회사를 택하는 기준도 나의 관심사를 기준으로 도메인을 선택하지 않던가..!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-20 오후 6.41.20.png&quot; data-origin-width=&quot;822&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Di2MV/btsu0FY1buD/xS3SDJRcylc5ZvrzJk7HR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Di2MV/btsu0FY1buD/xS3SDJRcylc5ZvrzJk7HR1/img.png&quot; data-alt=&quot;또 한 번의 폭파.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Di2MV/btsu0FY1buD/xS3SDJRcylc5ZvrzJk7HR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDi2MV%2Fbtsu0FY1buD%2FxS3SDJRcylc5ZvrzJk7HR1%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;373&quot; height=&quot;148&quot; data-filename=&quot;스크린샷 2023-09-20 오후 6.41.20.png&quot; data-origin-width=&quot;822&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;또 한 번의 폭파.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;5. 실패하더라도 남는게 있는가?&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;결국 이 프로젝트는 MVP는 나왔지만 포트폴리오에 쓰기에는 너무 작고 완성도가 떨어지는 상태로 폐기되었다.&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;&quot;2개월 동안 진짜 개고생했는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;남는 게 없는 것 같아서 속상하다&lt;/b&gt;.&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;(ㅎ.. 사람이 남았잖아요..!)&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;결국 우리 팀은 pivot 를 하고, '우리가 쓰고싶은 앱'을 만들기 위해서 더 즐거운 분위기로 새로운 프로젝트를 시작했다.&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;u&gt;&lt;b&gt;실패해도 좋을 만큼 그 안에서 평소에 하지 못했던 다양한 도전을 했어야 했다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;다양한 사람들 사이에 섞여서 개발 외적인 도전이라도 좋을 듯하다.&lt;/b&gt;&lt;/u&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;결국 2개월이라는 투자의 결과가 실패로 끝났지만, 그 시간이 매몰 비용으로 구분되지 않으려면 그 시간 속에서 다양한 경험들과 결과를 가져가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;매몰비용이 발생하지 않는 사이드 프로젝트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;1. 현실적으로 만들 수 있는 규모인가 ?&lt;br /&gt;2. 모두가 일정 시간 이상의 시간을 투자할 수 있는가 ?&lt;br /&gt;3. 팀원들이 프로젝트서 추구하는 목표가 같은가 ?&lt;br /&gt;4. 정말 '내가' 쓰고 싶은 서비스인가 ?&lt;br /&gt;5. 실패하더라도 남는 게 있는가 ?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 사이드 프로젝트에서 발생했던 문제들은 다양한 형태의 매몰 비용으로 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결정적으로 적절한 주제 선정부터 개인 또는 조직의 목표 선정이 필요하고, 개개인에게 지속 가능하도록 동기부여가 이루어져야 한다.&lt;/p&gt;
&lt;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&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.yes24.com/Product/Goods/92275597&quot;&gt;[넷플릭스 규칙없음]&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이라는 책이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책에서 말하는 두 가지 전제를 복기해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뛰어나고 혁신을 하고 싶은 사람들로 똘똘 뭉치게 밀도있는 팀을 만들 것&lt;/li&gt;
&lt;li&gt;솔직하게 피드백할 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전자도 중요한 것 같다. 확실하게 '의사'가 있고 구현할 수 있는 '능력'이 있어야 한다. (이건 이번 팀의 문제는 아니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;'솔직하게 피드백할 것'&lt;/b&gt;&lt;/u&gt;. 이게 가장 핵심적인 문구라고 생각된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 글에서 명시했던 문제들은 개선 또는 조율이 가능한 문제다. 그런데 이런 개선과 조율은 상호 피드백이 솔직담백하고 빠르게 오고 갔을 때 가능하다. 팀의 분위기가 좋지 않아질까봐 또는 개인에게 상처가 될까봐 이야기를 하지 않는 것 보다,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;모두가 늪에 빠져서 매몰 비용을 무한정 생성하지 않는 것이 중요하다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트 &amp;amp; 연간 회고</category>
      <author>깻묵(천진우)</author>
      <guid isPermaLink="true">https://netal.tistory.com/108</guid>
      <comments>https://netal.tistory.com/entry/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EB%A7%A4%EB%AA%B0%EB%B9%84%EC%9A%A9#entry108comment</comments>
      <pubDate>Tue, 12 Sep 2023 08:55:31 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Default 접근제어자로 견고한 테스트 만들기</title>
      <link>https://netal.tistory.com/entry/Java-Default-%EB%A7%A4%EC%84%9C%EB%93%9C%EB%A1%9C-%EB%B3%B5%EC%9E%A1%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%84%EC%86%8C%ED%99%94%ED%95%98%EA%B8%B0</link>
      <description>&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=7hQPQ9agDEg&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/dfmWVW/hyTMgG2KOL/crRxt0xdUbk9QQ0VDWC1y0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/7hQPQ9agDEg&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 아주 재미있게 즐겨보고 있는 이중석님의 유튜브입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올라온 당일날 봤었는데, 이번에 리팩토링을 진행하면서 딱 좋은 케이스가 생겨서 적용해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 코드는 어땠는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간소화해서 가져왔지만, 리팩토링 전의 코드기도 하고 Login 로직은 쬐끔 복잡합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OAuth provider 확인해서 적절한 bean 꺼내오기&lt;/li&gt;
&lt;li&gt;(외부 통신) OAuth provider에 요청 보내서, 유효한 로그인인가에 대한 검증 진행하기&lt;/li&gt;
&lt;li&gt;회원가입 한 적이 없다면 DB에 유저 생성하기&lt;/li&gt;
&lt;li&gt;(외부 통신) FCM 토큰 검증하고 저장하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1692814032007&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public Long login(String provider, LoginRequestDto requestDto) {
    OAuthProviderEnum providerEnum = OAuthProviderEnum.codeOf(provider);
    OAuthService oAuthService = oAuthServiceMap.get(providerEnum);
    OAuthUserInformation userInformation = oAuthService.requestUserInformation(requestDto.token());
    User user = getOrCreateUser(userInformation);
    if (requestDto.fcmDeviceToken() != null) {
        notificationService.registryFcmDeviceToken(user, requestDto.fcmDeviceToken());
    }
    return user.getId();
}

private User getOrCreateUser(OAuthUserInformation userInformation) {
    User user = null;
    Optional&amp;lt;User&amp;gt; target = userRepository.findByProviderAndProviderId(userInformation.getProvider(), userInformation.getProviderId());

    if (target.isPresent()) {
        log.info(&quot;UserServiceImpl|login(기존 회원): {}&quot;, userInformation);
        user = target.orElseThrow(() -&amp;gt; new PhochakException(ResCode.NOT_FOUND_USER));
    } else {
        log.info(&quot;UserServiceImpl|login(신규 회원): {}&quot;, userInformation);
        String nickname = generateInitialNickname();

        User newUser = User.builder()
                .provider(userInformation.getProvider())
                .providerId(userInformation.getProviderId())
                .nickname(nickname)
                .profileImgUrl(userInformation.getInitialProfileImage())
                .build();

        user = userRepository.save(newUser);
    }
    return user;
}&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_1692814365113&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;로그인 시 신규 회원이면 회원가입이 호출된다&quot;)
void login_newUser() {
    // given
    String code = &quot;testCode&quot;;
    String providerId = &quot;testProviderId&quot;;

    given(oAuthServiceMap.get(providerEnum)).willReturn(kakaoOAuthService);
    given(kakaoOAuthService.requestUserInformation(code)).willReturn(userInformation);
    given(userRepository.findByProviderAndProviderId(providerEnum, providerId)).willReturn(Optional.empty());
    given(userRepository.save(any())).willReturn(user);

    // when
    userService.login(&quot;kakao&quot;, code);

    // then
    then(userRepository).should(atLeastOnce()).save(any());
}

@Test
@DisplayName(&quot;로그인 시 기존 회원이면 회원가입이 호출되지 않는다&quot;)
void login_alreadyUser() {
    // given
    String code = &quot;testCode&quot;;
    String providerId = &quot;testProviderId&quot;;

    given(oAuthServiceMap.get(providerEnum)).willReturn(kakaoOAuthService);
    given(kakaoOAuthService.requestUserInformation(code)).willReturn(userInformation);
    given(userRepository.findByProviderAndProviderId(providerEnum, providerId)).willReturn(Optional.of(user));

    // when
    userService.login(&quot;kakao&quot;, code);

    // then
    then(userRepository).should(never()).save(any());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 코드는 무엇이 문제일까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 우리가 검증하고 싶은 &lt;b&gt;핵심 로직은&lt;/b&gt; getOrCreateUser() 매서드의 &lt;b&gt;&quot;해당 유저가 이미 가입한 유저인가?&quot;&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;그렇지만 getOrCreateUser() 매서드가 Private 이기 때문에 상위 매서드를 테스트 하고 있고, 상위 테스트는 너무나도 많은 의존성을 걸치고 있습니다.&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;그래서 위의 코드를 보면 외부 통신이나 불필요한 객체 등에 대해서 mocking 을 진행하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이후 Login 관련 로직의 변경으로 테스트가 깨질 확률이 높습니다.&lt;/li&gt;
&lt;li&gt;@Mock 으로 인한 테스트 클래스 자체의 선언부가 길어집니다&lt;/li&gt;
&lt;li&gt;각 테스트에서의 'given' 절에 불필요한 mocking 때문에 가독성이 떨어집니다.&lt;/li&gt;
&lt;li&gt;어떤 로직을 테스트 하고 싶은지가 코드 상으로 명확하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Default 생성자를 고려하자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 영상에서 소개하는 item은 &quot;로직을 default 매서드로 분리하고, 동일한 패키지에서 테스트하자.&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;그러면 getOrCreateUser() 를 default(package private) 로 선언하고 테스트를 진행해볼까요?&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;User getOrCreateUser(OAuthUserInformation userInformation) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;@Test
@DisplayName(&quot;로그인 시 신규 회원이면 회원가입이 호출된다&quot;)
void login_newUser() {
    // given
    given(userRepository.findByProviderAndProviderId(any(), any())).willReturn(Optional.empty());

    // when
    authService.getOrCreateUser(userInformation);

    // then
    then(userRepository).should(atLeastOnce()).save(any());
}

@Test
@DisplayName(&quot;로그인 시 기존 회원이면 회원가입이 호출되지 않는다&quot;)
void login_alreadyUser() {
    // given
    given(userRepository.findByProviderAndProviderId(any(), any())).willReturn(Optional.of(user));

    // when
    authService.getOrCreateUser(userInformation);

    // then
    then(userRepository).should(never()).save(any());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&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;375&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nZi8q/btsrZCyoGaE/BqnQYmWiR0jkkRRI7vt5Gk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nZi8q/btsrZCyoGaE/BqnQYmWiR0jkkRRI7vt5Gk/img.gif&quot; data-alt=&quot;깔-끔&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nZi8q/btsrZCyoGaE/BqnQYmWiR0jkkRRI7vt5Gk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/nZi8q/btsrZCyoGaE/BqnQYmWiR0jkkRRI7vt5Gk/img.gif&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;223&quot; height=&quot;240&quot; data-origin-width=&quot;375&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;깔-끔&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가 짧아진 것도 좋지만, 더 중요한 것은 &lt;b&gt;테스트가 명확해졌다는 점입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드를 설명하는 필수적인 mocking 만 given 절에 나타내고, 필요한 로직만을 검증하고 있습니다.&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;또한 상위 public 매서드의 변경으로 인해 &lt;b&gt;테스트가 깨지지 않게 됩니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 지속가능하고 유연한 테스트가 될 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;default 접근제어자를 통해 캡슐화를 최대한 지키면서 명확하고 견고한 테스트를 만들 수 있었습니다.&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;그러고보니 평소에 default 접근제어자에 대해서 무관심했고 의도적으로 사용해본 경험이 없었던 것 같네요.&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;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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1692815530775&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;[무료] 실전! 스프링부트 상품-주문 API 개발로 알아보는 TDD - 인프런 | 강의&quot; data-og-description=&quot;스프링부트를 이용해 TDD로 웹 애플리케이션을 만드는 방법을 배웁니다. TDD를 통해 개발 생산성을 유지하고 신뢰가능한 웹 애플리케이션을 만드는 방법을 얻으실 수 있을 겁니다., 실무 기반 TDD!&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8B%A4%EC%A0%84-%EC%83%81%ED%92%88%EC%A3%BC%EB%AC%B8-tdd/dashboard&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8B%A4%EC%A0%84-%EC%83%81%ED%92%88%EC%A3%BC%EB%AC%B8-tdd&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cRFkdn/hyTIE9AApr/UXY3pqiqFEiFjdFlKQ74dK/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bIyuz2/hyTIFtUFQ4/6rvRzrVzTIcjFxIRMl4cr0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8B%A4%EC%A0%84-%EC%83%81%ED%92%88%EC%A3%BC%EB%AC%B8-tdd/dashboard&quot; data-source-url=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8B%A4%EC%A0%84-%EC%83%81%ED%92%88%EC%A3%BC%EB%AC%B8-tdd/dashboard&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cRFkdn/hyTIE9AApr/UXY3pqiqFEiFjdFlKQ74dK/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bIyuz2/hyTIFtUFQ4/6rvRzrVzTIcjFxIRMl4cr0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;[무료] 실전! 스프링부트 상품-주문 API 개발로 알아보는 TDD - 인프런 | 강의&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;스프링부트를 이용해 TDD로 웹 애플리케이션을 만드는 방법을 배웁니다. TDD를 통해 개발 생산성을 유지하고 신뢰가능한 웹 애플리케이션을 만드는 방법을 얻으실 수 있을 겁니다., 실무 기반 TDD!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1692815530776&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;profile&quot; data-og-title=&quot;이중석&quot; data-og-description=&quot;&quot; data-og-host=&quot;www.youtube.com&quot; data-og-source-url=&quot;https://www.youtube.com/@ejoongseok&quot; data-og-url=&quot;https://www.youtube.com/channel/UCTeBkj0A6wjmIiUy-J4vGJg&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/TGiuQ/hyTIHrJvXl/ZlKVjhwdJ4DTkIKsvSoE2k/img.jpg?width=900&amp;amp;height=900&amp;amp;face=485_209_628_366,https://scrap.kakaocdn.net/dn/Rh2lp/hyTIJpxfNv/YPsfvrLCAuxP72rko2EuPK/img.jpg?width=900&amp;amp;height=900&amp;amp;face=485_209_628_366&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://www.youtube.com/@ejoongseok&quot; data-source-url=&quot;https://www.youtube.com/@ejoongseok&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/TGiuQ/hyTIHrJvXl/ZlKVjhwdJ4DTkIKsvSoE2k/img.jpg?width=900&amp;amp;height=900&amp;amp;face=485_209_628_366,https://scrap.kakaocdn.net/dn/Rh2lp/hyTIJpxfNv/YPsfvrLCAuxP72rko2EuPK/img.jpg?width=900&amp;amp;height=900&amp;amp;face=485_209_628_366');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;이중석&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;www.youtube.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;위의 방법 도입 후 5일 뒤 후기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확하게 게시글을 작성하고 5일 뒤에 변경사항이 생겼고 위의 코드가 의도대로 작동해서 신나게 가져왔습니다 ㅎㅎㅎ .&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA 엔티티와 도메인 객체를 구분한다.&lt;/li&gt;
&lt;li&gt;객체 Converter(Mapper)를 활용해서 두 객체를 전환한다.&lt;/li&gt;
&lt;li&gt;기존 OAuthService 를 기능으로 정의하고, port adapter 패턴으로 전환한다.&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;따라서 위의 로직을 CreateUserApdater 로 이관하고 Public으로 열어두는 작업을 진행했습니다.&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;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;@Override
public User getOrCreateUser(final OAuthUserInformation userInformation) {
    final Optional&amp;lt;UserEntity&amp;gt; target = userRepository.findByProviderAndProviderId(userInformation.getProvider(), userInformation.getProviderId());
    if (target.isPresent()) {
        log.info(&quot;CreateUserAdapter|getOrCreateUser(기존 회원 로그인): {}&quot;, userInformation);
        return userMapper.toDomain(target.get());
    } else {
        log.info(&quot;CreateUserAdapter|getOrCreateUser(신규 회원 가입): {}&quot;, userInformation);
        final UserEntity userEntity = userRepository.save(new UserEntity(
                userInformation.getProvider(),
                userInformation.getProviderId(),
                generateInitialNickname(),
                userInformation.getInitialProfileImage()
        ));
        return userMapper.toDomain(userEntity);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;분명 상위 매서드(UserService.createUser)의 로직이 바뀌게 되었고 getOrCreateUser의 자잘한 내부 구현이 바뀌게 되었습니다.&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;u&gt;&lt;b&gt;그러나 다음 이유 때문에 테스트의 로직은 깨지지 않았습니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;getOrCreateUser 를 별개로 테스트했다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트하려고 했던 핵심 로직은 변하지 않았다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 테스트였다면 저는 5일만에 테스트코드를 새로 작성하고 있었겠죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배운 내용을 코드에 적용하고, 빠르게 피드백을 받을 수 있는 경험이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <author>깻묵(천진우)</author>
      <guid isPermaLink="true">https://netal.tistory.com/107</guid>
      <comments>https://netal.tistory.com/entry/Java-Default-%EB%A7%A4%EC%84%9C%EB%93%9C%EB%A1%9C-%EB%B3%B5%EC%9E%A1%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%84%EC%86%8C%ED%99%94%ED%95%98%EA%B8%B0#entry107comment</comments>
      <pubDate>Thu, 24 Aug 2023 03:32:18 +0900</pubDate>
    </item>
    <item>
      <title>[DB] Pagination 실행계획 분석 (Offset Pagination vs Cursor Pagination)</title>
      <link>https://netal.tistory.com/entry/DB-Offset-Pagination-vs-Cursor-Pagination-%EC%84%B1%EB%8A%A5%EA%B3%BC-%EC%A0%9C%ED%95%9C%EC%82%AC%ED%95%AD-%EC%8B%AC%EC%B8%B5%EB%B6%84%EC%84%9D</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;토이프로젝트는 항상 신규 서비스를 만들다 보니, 메인페이지의 피드에 대한 구현 요구사항이 항상 있는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 1년동안 4개 서비스의 피드를 만들었네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 게시글은 제가 1년 조금 전에 처음으로 개발했던 피드에 대한 게시글입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://netal.tistory.com/entry/QueryDSL%EB%A1%9C-SNS-%ED%94%BC%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B01-%EB%8F%99%EC%A0%81-%EC%BF%BC%EB%A6%AC%EB%A1%9C-SNS-Pagination-%ED%94%BC%EB%93%9C%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;[DB] QueryDSL&lt;/b&gt;&lt;span&gt;&lt;b&gt;을&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;활용하여&lt;/b&gt;&lt;/span&gt;&lt;b&gt; SNS &lt;/b&gt;&lt;span&gt;&lt;b&gt;피드&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;만들기&lt;/b&gt;&lt;/span&gt;&lt;b&gt;(1) - &lt;/b&gt;&lt;span&gt;&lt;b&gt;동적&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;쿼리로&lt;/b&gt;&lt;/span&gt;&lt;b&gt; Pagination &lt;/b&gt;&lt;span&gt;&lt;b&gt;피드를&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;만들어보자&lt;/b&gt;&lt;/span&gt;&lt;b&gt;.&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://netal.tistory.com/entry/QueryDSL%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-SNS-%ED%94%BC%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B02-%EB%8D%94-%EC%A0%95%ED%99%95%ED%95%9C-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%BF%BC%EB%A6%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0-offset-based-cursor-based&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;[DB] QueryDSL&lt;/b&gt;&lt;span&gt;&lt;b&gt;을&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;활용하여&lt;/b&gt;&lt;/span&gt;&lt;b&gt; SNS &lt;/b&gt;&lt;span&gt;&lt;b&gt;피드&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;만들기&lt;/b&gt;&lt;/span&gt;&lt;b&gt;(2) - &lt;/b&gt;&lt;span&gt;&lt;b&gt;더&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;정확한&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;페이징&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;쿼리&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;만들기&lt;/b&gt;&lt;/span&gt;&lt;b&gt; offset based, cursor based&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1년이 지난 지금은 조금 더 다양한 관점에서의 분석이 가능할 것 같다는 생각이 들어서, 다음과 같은 내용을 중점으로 분석해보고자 합니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL Procedure 활용하여 테스트 환경 구축&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB: MySQL 8&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로시저로 더미데이터를 10만개 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1690198225589&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 유저 3명 생성
INSERT INTO USERS (id, nickname, profileImageUrl)
VALUES
    (1, 'user1', 'https://example.com/profiles/user1.jpg'),
    (2, 'user2', 'https://example.com/profiles/user2.jpg'),
    (3, 'user3', 'https://example.com/profiles/user3.jpg');

# 포스트 1만개 생성
DELIMITER //
CREATE PROCEDURE InsertData()
BEGIN
    DECLARE i INT DEFAULT 1;
    WHILE i &amp;lt;= 100000 DO
            INSERT INTO post (post_id, created_at, updated_at, category, content, image_url, user_id)
            VALUES (i, NULL, NULL, NULL, NULL, NULL, FLOOR(RAND() * 3) + 1);
            SET i = i + 1;
        END WHILE;
END //
DELIMITER ;

CALL InsertData();&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;auto-increament PK 를 사용하게 되면, B+ Tree 가 다음과 같이 구성되게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터링 인덱스(PK)의 순서가 게시글 업로드 순서를 보장하고, 최신 게시글이 가장 leaf 에 위치하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_9F27CC8E26DE-1.jpeg&quot; data-origin-width=&quot;2076&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PwRfa/btsoQucEnS5/cAshexANNopgz0XV0cjgEK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PwRfa/btsoQucEnS5/cAshexANNopgz0XV0cjgEK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PwRfa/btsoQucEnS5/cAshexANNopgz0XV0cjgEK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPwRfa%2FbtsoQucEnS5%2FcAshexANNopgz0XV0cjgEK%2Fimg.jpg&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;2076&quot; height=&quot;724&quot; data-filename=&quot;IMG_9F27CC8E26DE-1.jpeg&quot; data-origin-width=&quot;2076&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Offset Pagination 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Offset Pagination은 일반적으로 관계형 데이터베이스에서 제공하는 offset 기능을 활용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset은 결과 조회 시, N개의 결과를 건너뛰고 그 이후의 결과를 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 페이지로 건너뛰며 이동할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 게시글 방식에서 limit 만큼 보여주면서 limit 기준으로 page를 표기합니다.&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;IMG_EB0E7A5A2079-1.jpeg&quot; data-origin-width=&quot;2302&quot; data-origin-height=&quot;785&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XCOdG/btsoTn5PjTG/f3KkVuMEMUS93i3iNed6T0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XCOdG/btsoTn5PjTG/f3KkVuMEMUS93i3iNed6T0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XCOdG/btsoTn5PjTG/f3KkVuMEMUS93i3iNed6T0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXCOdG%2FbtsoTn5PjTG%2Ff3KkVuMEMUS93i3iNed6T0%2Fimg.jpg&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;2302&quot; height=&quot;785&quot; data-filename=&quot;IMG_EB0E7A5A2079-1.jpeg&quot; data-origin-width=&quot;2302&quot; data-origin-height=&quot;785&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 페이지를 알기 위해 앞의 데이터를 모두 탐색해야 합니다. 그래서 인덱스 풀스캔 또는 테이블 풀스캔을 하게 되기 때문에, 결과가 많다면 후반 페이지로 갈수록 조회 성능이 좋지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 구글 또는 네이버같은 검색엔진에서도 이러한 문제들 때문에 표시 가능한 페이지를 제한하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 일반적인 검색엔진에서 유저들에게는 15페이지 이상까지 탐색하지 않기 때문에 사용성에 큰 문제가 발생하지는 않을 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-25 오전 11.30.50.png&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTwN6q/btsoQucEJJx/s2VdvzhYwAPRYRyLIdYxNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTwN6q/btsoQucEJJx/s2VdvzhYwAPRYRyLIdYxNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTwN6q/btsoQucEJJx/s2VdvzhYwAPRYRyLIdYxNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTwN6q%2FbtsoQucEJJx%2Fs2VdvzhYwAPRYRyLIdYxNk%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;397&quot; height=&quot;119&quot; data-filename=&quot;스크린샷 2023-07-25 오전 11.30.50.png&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;119&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 만약 앞 페이지의 게시글이 새로 추가되거나 삭제된다면, 특정 게시글이 사라지거나 중복된 게시글이 표시될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cursor Pagination 특징&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;Cursor Pagination은 게시글 id 등의 고유 식별값을 입력으로 받고, 그 이후의 게시글을 불러옵니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 식별 값은 auto increament 로 구성된 PK 를 사용해야 성능을 기대할 수 있습니다.&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;마지막 게시글의 id &amp;gt; post.id&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력되는  식별자 값 부터 탐색하면 되기 때문에, 앞부분을 읽지 않아도 되며 offset pagination에 비해 성능상에 이점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 페이지에 게시글이 추가되거나 삭제되어도 게시글 중복과 누락이 발생하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 페이지로 이동하는 기능을 제공할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 facebook, twitter, instagram 등 소셜네트워크 앱에서 많이 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 컨텐츠를 연속적으로 빠르게 불러와야하는 infinity scrolling 구현에 적합합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-25 오전 11.30.29.png&quot; data-origin-width=&quot;431&quot; data-origin-height=&quot;109&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4wb8z/btsoKYZDWbj/hhFajCnhXn28801EfuKx9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4wb8z/btsoKYZDWbj/hhFajCnhXn28801EfuKx9K/img.png&quot; data-alt=&quot;15페이지 까지만 조회할 수 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4wb8z/btsoKYZDWbj/hhFajCnhXn28801EfuKx9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4wb8z%2FbtsoKYZDWbj%2FhhFajCnhXn28801EfuKx9K%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;431&quot; height=&quot;109&quot; data-filename=&quot;스크린샷 2023-07-25 오전 11.30.29.png&quot; data-origin-width=&quot;431&quot; data-origin-height=&quot;109&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;15페이지 까지만 조회할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&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;실행 계획 살펴보기 - Offset Pagination&lt;/h2&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;그래서 이번 테스트 쿼리에서는 1번 유저가 차단된 유저라고 생각하고 다음 조건을 넣어보겠습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;p1_0.user_id != 1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;(접기) QueryDSL 코드&lt;br /&gt;
&lt;pre id=&quot;code_1690276055704&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public List&amp;lt;FeedResponseDto&amp;gt; getFeedOffset() {
    return queryFactory
            .select(new QFeedResponseDto(
                    post.id,
                    post.content,
                    post.imageUrl,
                    post.category,
                    post.createdAt,
                    post.updatedAt,
                    user.id,
                    user.nickname,
                    user.profileImageUrl
            ))
            .from(post)
            .join(post.user, user)
            .where(
                    post.user.id.ne(1L) // some condition
            )
            .orderBy(post.id.desc())
            .offset(30000L) // offset pagination
            .limit(10)
            .fetch();
}​&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1690269404767&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# offset

explain SELECT p1_0.post_id,
       p1_0.content,
       p1_0.image_url,
       p1_0.category,
       p1_0.created_at,
       p1_0.updated_at,
       p1_0.user_id,
       u1_0.nickname,
       u1_0.profile_image_url
FROM   post p1_0
           JOIN users u1_0
                ON u1_0.user_id = p1_0.user_id
WHERE  p1_0.user_id != 1
ORDER  BY p1_0.post_id DESC
LIMIT 60000, 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 offset 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시글 번호가 100,000까지 있고, 1번의 게시글(전체 비중의 33.3%)이 조회되지 않는다고 하면, offset 60000 은 거의 마지막에 가까운 게시글입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-25 오후 4.00.05.png&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;819&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EAmJL/btsoULys24Z/7UEt90ejAIifbU8KZs7HeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EAmJL/btsoULys24Z/7UEt90ejAIifbU8KZs7HeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EAmJL/btsoULys24Z/7UEt90ejAIifbU8KZs7HeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEAmJL%2FbtsoULys24Z%2F7UEt90ejAIifbU8KZs7HeK%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;421&quot; height=&quot;554&quot; data-filename=&quot;스크린샷 2023-07-25 오후 4.00.05.png&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;819&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key: PRIMARY , type: index 인 것을 보아 PK를 인덱스 스캔을 했습니다.&lt;/li&gt;
&lt;li&gt;그런데 사실 PK가 데이터가 실제 저장된 구조와 같기 때문에, PK의 인덱스 스캔은 사실상 풀테이블 스캔과 동작이 동일합니다.&lt;/li&gt;
&lt;li&gt;인덱스(PK)를 역탐색 하기 때문에 Backward index scan이 표기됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;혹시라도 PK가 아니라 일반 인덱스를 역순으로 탐색하기 위해서는, 인덱스 자체를 역순으로 생성해야 인덱스의 도움을 받을 수 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 동작을 살펴보면..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-25 오후 4.27.27.png&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;79&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EumMB/btsoSj3xTWl/wGkidVexkKvefkrmbAQ9z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EumMB/btsoSj3xTWl/wGkidVexkKvefkrmbAQ9z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EumMB/btsoSj3xTWl/wGkidVexkKvefkrmbAQ9z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEumMB%2FbtsoSj3xTWl%2FwGkidVexkKvefkrmbAQ9z1%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;783&quot; height=&quot;87&quot; data-filename=&quot;스크린샷 2023-07-25 오후 4.27.27.png&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;79&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;PK 에 Order By DESC 가 걸려있으니 가장 마지막 PK를 찾아갑니다. (Backward index scan)&lt;/li&gt;
&lt;li&gt;위의 그림에서 offset 이 4라고 가정하면, 조건에 맞는 데이터를 4개 만날 때 까지&amp;nbsp;&lt;/li&gt;
&lt;li&gt;offset 탐색이 끝나면 버퍼에 결과를 쌓습니다.&lt;/li&gt;
&lt;li&gt;limit 을 채울 때까지 탐색하다가, 충족되면 탐색을 끝냅니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;실행 계획 살펴보기 - Cursor Pagination&lt;/h2&gt;
&lt;pre id=&quot;code_1690269387598&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# cursor
explain SELECT p1_0.post_id,
       p1_0.content,
       p1_0.image_url,
       p1_0.category,
       p1_0.created_at,
       p1_0.updated_at,
       p1_0.user_id,
       u1_0.nickname,
       u1_0.profile_image_url
FROM   post p1_0
           JOIN users u1_0
                ON u1_0.user_id = p1_0.user_id
WHERE  p1_0.post_id &amp;lt; 1000
  AND p1_0.user_id != 1
ORDER  BY p1_0.post_id DESC
LIMIT  10;&lt;/code&gt;&lt;/pre&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;(접기) QueryDSL 코드&lt;/p&gt;
&lt;pre id=&quot;code_1690276105484&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public List&amp;lt;FeedResponseDto&amp;gt; getFeedCursor() {
    return queryFactory
            .select(new QFeedResponseDto(
                    post.id,
                    post.content,
                    post.imageUrl,
                    post.category,
                    post.createdAt,
                    post.updatedAt,
                    user.id,
                    user.nickname,
                    user.profileImageUrl
            ))
            .from(post)
            .join(post.user, user)
            .where(
                    post.id.lt(1000L), // cursor pagination
                    post.user.id.ne(1L) // some condition
            )
            .orderBy(post.id.desc())
            .limit(10)
            .fetch();
}&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-25 오후 6.10.59.png&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;97&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBPflJ/btsoTl1otdh/pwnl8xHZCF4K9JdgjAyITK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBPflJ/btsoTl1otdh/pwnl8xHZCF4K9JdgjAyITK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBPflJ/btsoTl1otdh/pwnl8xHZCF4K9JdgjAyITK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBPflJ%2FbtsoTl1otdh%2Fpwnl8xHZCF4K9JdgjAyITK%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;708&quot; height=&quot;97&quot; data-filename=&quot;스크린샷 2023-07-25 오후 6.10.59.png&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;97&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;type: range , row: 999 입니다. 인덱스 레인지 스캔을 통해 탐색 시작 컬럼을 바로 찾아갔습니다.&lt;/li&gt;
&lt;li&gt;그 외 특징은 offset과 동일합니다.&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;IMG_1AF1EFF7ABC3-1.jpeg&quot; data-origin-width=&quot;2335&quot; data-origin-height=&quot;781&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DPOoe/btsoY7VkJjo/xOlrns5eJlaRbmnJPrNRh0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DPOoe/btsoY7VkJjo/xOlrns5eJlaRbmnJPrNRh0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DPOoe/btsoY7VkJjo/xOlrns5eJlaRbmnJPrNRh0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDPOoe%2FbtsoY7VkJjo%2FxOlrns5eJlaRbmnJPrNRh0%2Fimg.jpg&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;2335&quot; height=&quot;781&quot; data-filename=&quot;IMG_1AF1EFF7ABC3-1.jpeg&quot; data-origin-width=&quot;2335&quot; data-origin-height=&quot;781&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt; 인덱스를 통해 탐색을 시작할 위치를 바로 찾아갑니다.&amp;nbsp; (Index Range Scan)&lt;/li&gt;
&lt;li&gt;limit 을 채울 때까지 탐색하다가, 충족되면 탐색을 끝냅니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 offset 처럼 앞의 게시글들을 탐색할 필요가 없으니, index 탐색의 뒷쪽에 있는 컨텐츠를 탐색할수록 cursor pagination 의 장점이 부각되는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB</category>
      <author>깻묵(천진우)</author>
      <guid isPermaLink="true">https://netal.tistory.com/105</guid>
      <comments>https://netal.tistory.com/entry/DB-Offset-Pagination-vs-Cursor-Pagination-%EC%84%B1%EB%8A%A5%EA%B3%BC-%EC%A0%9C%ED%95%9C%EC%82%AC%ED%95%AD-%EC%8B%AC%EC%B8%B5%EB%B6%84%EC%84%9D#entry105comment</comments>
      <pubDate>Fri, 21 Jul 2023 18:45:31 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] 람다(lambda) 의 작동 방식과 지역변수 사용(capturing lambda)</title>
      <link>https://netal.tistory.com/entry/JAVA-%EB%9E%8C%EB%8B%A4lambda-%EC%9D%98-%EC%9E%91%EB%8F%99-%EB%B0%A9%EC%8B%9D%EA%B3%BC-%EC%A7%80%EC%97%AD%EB%B3%80%EC%88%98-%EC%82%AC%EC%9A%A9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 면접에서 질문 받았던 내용에 답변을 못했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;자바 람다 에서 지역변수를 사용하려면 어떻게 해야하나요?&quot;&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;지나간 면접은 잊고, 오늘은 람다가 어떻게 동작하는지, 그렇다면 지역변수를 어떻게 참조하는지에 대해서 알아보겠습니다.&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;람다(Lambda)는 매서드 명이 없는 간결한 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java8 에서부터 등장하였으며, 주로 함수형 프로그래밍에서 함수를 인자로 넘길 때 간결하게 표현할 수 있어서 사용됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&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;livescript&quot;&gt;&lt;code&gt;public class Test {
    public static void main(String[] args) {
        List&amp;lt;String&amp;gt; names = new ArrayList&amp;lt;&amp;gt;();
        names.add(&quot;Ethan&quot;);
        names.add(&quot;Olivia&quot;);
        names.add(&quot;Liam&quot;);

        List&amp;lt;String&amp;gt; upperCaseNameList = convertNames(names, (name) -&amp;gt; name.toUpperCase());
        System.out.println(&quot;대문자 변환: &quot; + upperCaseNameList);

        List&amp;lt;String&amp;gt; lowerCaseNameList = convertNames(names, (name) -&amp;gt; name.toLowerCase());
        System.out.println(&quot;소문자 변환: &quot; + lowerCaseNameList);
    }

    public static &amp;lt;T, R&amp;gt; List&amp;lt;R&amp;gt; convertNames(List&amp;lt;T&amp;gt; names, Function&amp;lt;T, R&amp;gt; converter) {
        List&amp;lt;R&amp;gt; result = new ArrayList&amp;lt;&amp;gt;();
        for (T name : names) {
            R convertedName = converter.apply(name);
            result.add(convertedName);
        }
        return result;
    }
}&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 class=&quot;dart&quot;&gt;&lt;code&gt;// List&amp;lt;String&amp;gt; upperCaseNameList = convertNames(names, (name) -&amp;gt; name.toUpperCase());

List&amp;lt;String&amp;gt; upperCaseNameList = convertNames(names, new Function&amp;lt;String, String&amp;gt;() {
    @Override
    public String apply(String name) {
        return name.toUpperCase();
    }
});&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;그렇지만 만약 람다식 안에 들어가는 로직이 길고 복잡해지면, 오히려 가독성을 해치는 것 같은 느낌을 받아서 주석을 추가하거나 함수로 분리하는 것이 좋았던 것 같습니다.&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;위에서 나온 Function&amp;lt;T, R&amp;gt; 클래스는 무엇일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수를 매개변수로 받고, apply() 매서드를 통해 실행된 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;함수형 인터페이스는 추상 매서드가 단 하나만 존재하는 인터페이스입니다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다 표현식은 이 인터페이스를 통해 하나의 매서드를 실행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 사용된 Fucntion 은 자바에서 제공하는 함수형 인터페이스 중 하나로, T 타입 인자를 받아서 R 타입을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 내부를 볼까요?&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface Function&amp;lt;T, R&amp;gt; {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

    ...

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 람다를 사용하지 않은 코드를 보면, apply()를 override 해서 사용했던거 기억하시나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부에는 R apply(T t)를 구현하여 사용하게 스팩만 정의되어 있습니다.&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;그리고 @FunctionalInterface 는 컴파일러에게  함수형 인터페이스임을 알려주고, 위에서 언급한 형태에 맞는지 에러를 잡아냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서는 기본적으로 다양한 함수형 인터페이스를 제공하고 있으며, 여기에서 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html&quot;&gt;https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 지역변수를 람다에서 사용하려면..&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 람다의 사용법에 대해서 알아봤으니, 람다에서 외부 변수를 어떻게 사용하는지에 대해서 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688324146886&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    int message = 1; // 정의
    message = 2; // 값 변경
    
    Runnable runnable = () -&amp;gt; {
        System.out.println(message); // 지역변수 출력
    };
    new Thread(runnable).start();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드를 실행시키면, 다음과 같은 에러가 발생합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;error:&amp;nbsp;local&amp;nbsp;variables&amp;nbsp;referenced&amp;nbsp;from&amp;nbsp;a&amp;nbsp;lambda&amp;nbsp;expression&amp;nbsp;must&amp;nbsp;be&amp;nbsp;final&amp;nbsp;or&amp;nbsp;effectively&amp;nbsp;final&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면, 람다에서의 지역 변수 참조는 변경 가능성이 없는 불변객체여야합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1688325367219&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
   final int message1 = 1; // 명시적인 Final
   
   int message2 = 1; // 묵시적인 Final (Effective final)
   
   ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 위와 같이 Final로 명시적으로 선언하거나, Final을 붙이지는 않지만 이후에 변경하지 않는 effective final의 형태가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런식으로 설계가 되었는지 궁금하지 않으신가요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;캡쳐링 람다 (Capturing Lambda)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그 이유는, 참조하고 싶은 지역 변수의 값이 없어질 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 무슨 소리일까요?&lt;/p&gt;
&lt;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;스크린샷 2023-07-03 오전 4.13.18.png&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GgIOG/btsl2d58ZLd/RdVAj7nuBOrmvm6lY89x20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GgIOG/btsl2d58ZLd/RdVAj7nuBOrmvm6lY89x20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GgIOG/btsl2d58ZLd/RdVAj7nuBOrmvm6lY89x20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGgIOG%2Fbtsl2d58ZLd%2FRdVAj7nuBOrmvm6lY89x20%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;559&quot; height=&quot;333&quot; data-filename=&quot;스크린샷 2023-07-03 오전 4.13.18.png&quot; data-origin-width=&quot;910&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;이 그림이 아마 저희가 원하는 그림일 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자 다른 스레드라면, 해당 스레드의 지역변수는 별개의 스택에 저장되고 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 별개의 스레드는 언제 끝날지 모르기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다른 스레드에서 기존의 스택을 참조하려고 했을 때 그 스택이 이미 종료되었을 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-03 오전 4.21.13.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbAOx4/btsl2clRv64/5VpotMqwnb1ayfneyZpl5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbAOx4/btsl2clRv64/5VpotMqwnb1ayfneyZpl5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbAOx4/btsl2clRv64/5VpotMqwnb1ayfneyZpl5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbAOx4%2Fbtsl2clRv64%2F5VpotMqwnb1ayfneyZpl5K%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;537&quot; height=&quot;323&quot; data-filename=&quot;스크린샷 2023-07-03 오전 4.21.13.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;578&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그래서 자바에서는 '캡쳐링 람다'를 통해 해당 지역변수의 복사본을 저장하고, 람다식 내에서는 해당 복사본을 참조합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 capturing lambda 의 방식을 통해 해당 변수가 변경되지 않음을 보장합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-03 오전 4.28.36.png&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;517&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7V6iK/btsl6VDqVPK/DKOh7NeNhVAdM3izZ5wiu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7V6iK/btsl6VDqVPK/DKOh7NeNhVAdM3izZ5wiu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7V6iK/btsl6VDqVPK/DKOh7NeNhVAdM3izZ5wiu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7V6iK%2Fbtsl6VDqVPK%2FDKOh7NeNhVAdM3izZ5wiu0%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;735&quot; height=&quot;416&quot; data-filename=&quot;스크린샷 2023-07-03 오전 4.28.36.png&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;517&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;참고로 컬렉션 프레임워크나 인스턴스 및 클래스 변수는 Method Area와 Heap 에 저장되기 때문에 참조가 가능합니다.&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;그러나 동시성 문제가 발생할 수 있으니, 필요에 따라 동시성을 보장하는 자료형이나 volatile 등을 사용하는게 좋겠습니다.&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;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;자바 람다 에서 지역변수를 사용하려면 어떻게 해야하나요?&quot;&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;복기해보니, 이 질문 하나로 지원자의 많은 것을 파악할 수 있었던 것 같습니다.&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;JVM 의 메모리 구조에 대해서 잘 알고 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉽기도 하지만, 사실 면접 경험 자체가 너무 훌륭했어서 이후에 복기하고 공부하면서 많은 것을 배운 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;467&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crXxVS/btsl1JxvMLv/Jrv3manKdwlpKfNfPhaIX0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crXxVS/btsl1JxvMLv/Jrv3manKdwlpKfNfPhaIX0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crXxVS/btsl1JxvMLv/Jrv3manKdwlpKfNfPhaIX0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrXxVS%2Fbtsl1JxvMLv%2FJrv3manKdwlpKfNfPhaIX0%2Fimg.jpg&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;347&quot; height=&quot;190&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;467&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;</description>
      <category>Java</category>
      <author>깻묵(천진우)</author>
      <guid isPermaLink="true">https://netal.tistory.com/104</guid>
      <comments>https://netal.tistory.com/entry/JAVA-%EB%9E%8C%EB%8B%A4lambda-%EC%9D%98-%EC%9E%91%EB%8F%99-%EB%B0%A9%EC%8B%9D%EA%B3%BC-%EC%A7%80%EC%97%AD%EB%B3%80%EC%88%98-%EC%82%AC%EC%9A%A9#entry104comment</comments>
      <pubDate>Mon, 3 Jul 2023 02:24:27 +0900</pubDate>
    </item>
  </channel>
</rss>