<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>매일매일 꾸준히</title>
    <link>https://junuuu.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 31 May 2026 06:33:17 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Junuuu</managingEditor>
    <image>
      <title>매일매일 꾸준히</title>
      <url>https://tistory1.daumcdn.net/tistory/5021362/attach/4acc086361fb4f0899e91533faff3349</url>
      <link>https://junuuu.tistory.com</link>
    </image>
    <item>
      <title>connection prematurely close BEFORE response 해결기</title>
      <link>https://junuuu.tistory.com/1049</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;reactor netty를 활용하는 Spring Cloud Gateway 서버에서 간헐적으로 connection prematurely close BEFORE response 오류가 발생하고 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 서버는 200 tps 정도로 요청을 처리하는데 하루에 간헐적으로 1~2건 정도 오류가 발생하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;어떤 에러인지?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에러 메시지를 해석해 보면 connection이 response를 받기 전에 close 되었다는 예외 메시지입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;왜 요청 중인 connection이 close 되었는지를 파악한다면 문제를 해결할 수 있을 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;환경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1773326712393&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;reactor-netty 1.1.13

jvm 17

spring-cloud-starter-gateway 4.0.9

envoy proxy 활용하는 pod to pod 통신과정에서 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Reactor Netty Debugging Guide&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 오류는 reactor-netty 커뮤니티에서 자주 issue에 올라오기 때문에 공식문서에 FQA 가이드가 정리되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://projectreactor.io/docs/netty/release/reference/faq.html#faq.connection-closed&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://projectreactor.io/docs/netty/release/reference/faq.html#faq.connection-closed&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773326788827&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;Frequently Asked Questions :: Reactor Netty Reference Guide&quot; data-og-description=&quot;By default, Reactor Netty uses direct memory as this is more performant when there are many native I/O operations (working with sockets), as this can remove the copying operations. As allocation and deallocation are expensive operations, Reactor Netty also&quot; data-og-host=&quot;projectreactor.io&quot; data-og-source-url=&quot;https://projectreactor.io/docs/netty/release/reference/faq.html#faq.connection-closed&quot; data-og-url=&quot;https://projectreactor.io/docs/netty/release/reference/faq.html#faq.connection-closed&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://projectreactor.io/docs/netty/release/reference/faq.html#faq.connection-closed&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://projectreactor.io/docs/netty/release/reference/faq.html#faq.connection-closed&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;Frequently Asked Questions :: Reactor Netty Reference Guide&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;By default, Reactor Netty uses direct memory as this is more performant when there are many native I/O operations (working with sockets), as this can remove the copying operations. As allocation and deallocation are expensive operations, Reactor Netty also&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;projectreactor.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;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본적으로 reactor-netty의 Clients는 connection pool을 활용하며, 유효한 connection을 얻어온 후 다양한 이유로 종료될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 여러 가지를 확인하면 문제를 해결할 수 있다고 가이드합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. TCP dump를 통하여 FIN/RST signal이 상대 서버로부터 오는지 확인하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. network connection 확인하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. 방화벽이나 VPN 활용하는지 확인하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. 프록시나 로드밸런서를 활용하는지, client와 target server의 idle timeout 설정은 얼마인지&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. target server의 메모리 제한, max file limit size, bad request, max keep alive requests 는 얼마인지&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;HTTP 1.1 Keep Alive idle Timeout&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;keep alive idle timeout은 HTTP 연결을 재사용하기 위해 열어둔 TCP connection을 얼마나 오래 유휴 상태로 유지할지를 결정하는 시간입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청마다 TCP connection을 만들지 않기 때문에 오버헤드를 줄일 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;gateway server의 idle timeout&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- max-idle-time: 3595000&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- max-life-time: 60s&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- eviction-interval: 30s&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;idle timeout은 3595초로 정의되어 있지만 max-life-time 설정에 의하여 60초마다 connection이 정리됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;envoy proxy의 idle timeout&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 3600s&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;target server의 idle timeout&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 3605s&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;connection을 유지하는 시간이 gateway server &amp;lt; envoy proxy &amp;lt; target server 로 잘 구성되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약에 gateway server &amp;gt; envoy proxy 였다면 gateway server가 connection을 획득했을 때 envoy proxy는 idle timeout 이 발생하여 gateway server로 FIN / RST 패킷을 보내면서 connection prematurely close BEFORE response 예외가 발생할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;TCP dump&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;idle timeout 설정상으로는 이슈가 발생하지 않아야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 정확한 원인을 파악하기 위해서 TCP dump도 수행하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;gateway -&amp;gt; target server 구간 TCP dump&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. gateway -&amp;gt; target server HTTP 요청&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. target server -&amp;gt; gateway -&amp;gt; tcp ack&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. gateway server 에 &lt;span style=&quot;text-align: start;&quot;&gt;connection prematurely close BEFORE response 로그 발생&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;4. gateway server -&amp;gt; target server로 FIN/ACK 요청&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;5. target server -&amp;gt; gateway server로 FIN/ACK 요청&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;target server에서 FIN을 보내는 것이 아니라 gateway server에서 먼저 FIN/ACK을 보내서 연결을 종료하고 있는 것을 확인할 수 있었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;gateway -&amp;gt; target server 구간 통신에는 envoy proxy가 존재하기 때문에 해당 구간에도 TCP dump를 수행해 보았지만 결과는 동일하게 gateway에서 먼저 FIN/ACK를 보내고 있었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;client -&amp;gt; gateway 구간 TCP dump&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;클라이언트가 응답 수신 전에 연결을 끊거나 요청을 취소하는 경우를 의심해 볼 수 있지만 TCP dump 상 Client에서 받은 FIN은 확인되지 않았습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;그리고 연결을 끊었다면 gateway에서 응답을 받을 필요가 없을 텐데, 500 HTTP status code를 응답받고 있었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Envoy Access Log&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;envoy proxy도 로그를 남기고 있어 확인해 보았을 때 gateway -&amp;gt; target server로 DC 로그가 남아있었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;DC는 Downstream Connection Termination으로 클라이언트가 서버의 응답을 받기 전에 끊었다는 것입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;여기까지 확인했을 때 gateway 내부에서 뭔가 취소가 발생하고 있는 것 같습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Log DEBUG level&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1773328127076&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;logging.level.reactor.netty.resources.PooledConnectionProvider=DEBUG
logging.level.reactor.netty.resources.DefaultPooledConnectionProvider=DEBUG
logging.level.reactor.netty.http.client.HttpClientOperations=DEBUG
logging.level.reactor.netty.channel.ChannelOperationsHandler=DEBUG&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;connection pool을 가져오고 반납하는 과정에서 race condition이 의심되어 DEBUG 로그를 활성화하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;오류가 발생하는 타임라인은 아래와 같았습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. connection 최초 생성&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 하나의 connection을 24번 활용하고 connection pool에 반납&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. connection pool에서 connection을 꺼내와서 25번째 요청 시작 (request_sent)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. Channel close 발생&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. response_incomplete&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. The connection observed an error&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이로써 유효한 connection을 가져오긴 했지만, 요청하고 응답을 받기 전 어디선가 취소가 되었음을 파악할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한 25번째 요청마다 문제가 발생하진 않고, 16번째 요청에 발생하기도 하며, 82번째 요청에 발생하기도 하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;reactor.netty.http.client.HttpClientConnect&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1773329055025&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// In some cases the channel close event may be delayed and thus the connection to be
// returned to the pool and later the eviction functionality to remove it from the pool.
// In some rare cases the connection might be acquired immediately, before the channel close
// event and the eviction functionality is able to remove it from the pool; this may lead to I/O
// errors.
// Mark the connection as non-persistent here so that it is never returned to the pool and leave
// the channel close event to invalidate it.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HttpClientConnect 클래스의 주석을 살펴보면 몇몇 케이스에서 channel close event는 지연될 수 있다고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 connection pool에서 connection을 가져왔지만 pool에서 제거되고 I/O 에러로 이어질 수 있다고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 통하여 특정 상황에서 target server에서 Connection: close가 응답으로 내려와서 connection pool에서 evict 되어야 하지만 이 부분이 지연되어 다음 요청 시에 I/O 에러가 관측된 것을 아닐까? 라는 가설을 세웠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다만 이전 요청의 Response 응답 시 헤더를 관찰했을 때 Connection 헤더는 관찰되지 않았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;이전 요청에는 특이사항은 없었나?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;헤더 값 이외의 과거 오류가 발생했던 건들의 이전 요청에 특이사항을 확인해 보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 client로 부터 CANCEL signal 로그가 확인되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;N번째 요청에서 예외가 발생했을 때 N-1 번째 요청에서 Client의 취소 요청이 유입되고 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이로써 이전 요청의 취소가 현재 요청에 영향을 준다고 가설을 세울 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;어떻게 이전 요청의 취소가 현재 요청에 영향을 줄 수 있을까?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청 A (N-1 번째)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; 브라우저 -&amp;gt; Gateway -&amp;gt; Target server&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; Target server 응답 완료 Gateway가 connection pool에 반납&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; Gateway -&amp;gt; 브라우저 응답 전송 중 (아직 완료 안 됨)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청 B (N번째)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; Pool에 반납된 connection을 재사용하여 Target Server로 요청 전송 중&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 시점에 브라우저가 요청 A를 취소 ( 탭 닫기, 새로고침 등)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;-&amp;gt; Gateway는 요청 A의 처리를 취소하려 함&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;-&amp;gt; 요청 A의 취소 신호가 요청 B가 사용 중인 connection을 닫아버림&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;-&amp;gt; PrematureCloseException 발생&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;로컬에서 재현해보기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;준비과정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 임의의 route를 등록해두고, 로컬에서 8081 포트로 3초 지연되는 python 서버를 둔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 만들어둔 route로 요청을 보내면 3초 뒤에 응답이 온다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. reactor.netty.channel.ChannelOperations dispose 메서드에 디버깅을 찍는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 디버깅 시에는 요청받은 Thread만 막아두도록 설정하여 다음 요청이 처리될 수 있도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;재현과정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. http 요청을 보내고 3초 전에 취소를 한다 (Ctrl + C)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 디버깅 포인트로 이동 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. http 요청을 보내고 3초가 지나기 전에 디버깅을 재개&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;해결방안&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;reactor-netty main 브랜치를 받아서 로컬에서 재현을 위한 테스트 메서드를 구현하다 보니 1.1.24 버전을 활용하면 이 부분은 해결됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구성한 테스트 메서드는 1.1.24 이후 버전부터는 성공하여, 1.1.23 버전에서는 실패합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1.1.24 버전에 PR #3459가 반영되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;memory leak을 막기 위한 수정이였지만 DisposedConnection / DisposedChannel 개념이 도입되면서 이전 요청의 Connection 취소가 더 이상 영향을 주지 않게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/reactor/reactor-netty/pull/3459&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/reactor/reactor-netty/pull/3459&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773330300343&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;When terminating detach the connection from request/response objects by violetagg &amp;middot; Pull Request #3459 &amp;middot; reactor/reactor-netty&quot; data-og-description=&quot;Related to #3416, #3367&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/reactor/reactor-netty/pull/3459&quot; data-og-url=&quot;https://github.com/reactor/reactor-netty/pull/3459&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bFIpgH/dJMb9jOlpwR/rkhsVq4KUWKF0QyOAYpeX1/img.png?width=1200&amp;amp;height=600&amp;amp;face=978_118_1053_200,https://scrap.kakaocdn.net/dn/5HoNU/dJMb9c9wjrD/AvQ0QyvpLwRB3ldyRQx5O0/img.png?width=1200&amp;amp;height=600&amp;amp;face=978_118_1053_200&quot;&gt;&lt;a href=&quot;https://github.com/reactor/reactor-netty/pull/3459&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/reactor/reactor-netty/pull/3459&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bFIpgH/dJMb9jOlpwR/rkhsVq4KUWKF0QyOAYpeX1/img.png?width=1200&amp;amp;height=600&amp;amp;face=978_118_1053_200,https://scrap.kakaocdn.net/dn/5HoNU/dJMb9c9wjrD/AvQ0QyvpLwRB3ldyRQx5O0/img.png?width=1200&amp;amp;height=600&amp;amp;face=978_118_1053_200');&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;When terminating detach the connection from request/response objects by violetagg &amp;middot; Pull Request #3459 &amp;middot; reactor/reactor-netty&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Related to #3416, #3367&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Reactor Netty PR 올리기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;재발방지를 위한 회귀 테스트를 구현하였고, 다른이의 삽질을 방지하기 위해 FAQ에 문서화를 보강하여 PR을 올렸습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다만 1.1.x는 더 이상 지원하지 않는 버전이여서 문서에는 넣지 않았으면 좋겠다는 피드백을 받고 회귀 테스트만 반영하게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a href=&quot;https://github.com/reactor/reactor-netty/pull/4137&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/reactor/reactor-netty/pull/4137&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773330569154&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;Add regression test for stale dispose closing reused connection by Junuu &amp;middot; Pull Request #4137 &amp;middot; reactor/reactor-netty&quot; data-og-description=&quot;Summary When a previous request's cancel signal is processed after the connection has been returned to the pool and reused by a new request, the delayed dispose() call may incorrectly close the...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/reactor/reactor-netty/pull/4137&quot; data-og-url=&quot;https://github.com/reactor/reactor-netty/pull/4137&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bl6UQE/dJMb9hCZLui/5XKgYmUVzi7HAtmCwBE1j1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bazas2/dJMb9efcoB8/qIorMb5s6zLVKoXGSQ21s0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/reactor/reactor-netty/pull/4137&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/reactor/reactor-netty/pull/4137&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bl6UQE/dJMb9hCZLui/5XKgYmUVzi7HAtmCwBE1j1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bazas2/dJMb9efcoB8/qIorMb5s6zLVKoXGSQ21s0/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;Add regression test for stale dispose closing reused connection by Junuu &amp;middot; Pull Request #4137 &amp;middot; reactor/reactor-netty&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Summary When a previous request's cancel signal is processed after the connection has been returned to the pool and reused by a new request, the delayed dispose() call may incorrectly close the...&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring Framework/Spring Cloud Gateway</category>
      <author>Junuuu</author>
      <guid isPermaLink="true">https://junuuu.tistory.com/1049</guid>
      <comments>https://junuuu.tistory.com/1049#entry1049comment</comments>
      <pubDate>Fri, 13 Mar 2026 00:50:28 +0900</pubDate>
    </item>
    <item>
      <title>Tomcat 에서 발생하는 간헐적 404 해결기</title>
      <link>https://junuuu.tistory.com/1048</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Spring Boot embedded-tomcat 환경의 특정 서버에서 404 응답이 발생하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 원인을 분석해서 해결한 내용을 공유해보고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;현상&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;A -&amp;gt; B 서버로 동일한 http 요청에도 불구하고 6건 중 5건은 성공하고 1건이 실패하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실패했을 때의 응답은 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1768631778071&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;title&amp;gt;HTTP Status 404 &amp;ndash; Not Found&amp;lt;/title&amp;gt;

    &amp;lt;style type=&quot;text/css&quot;&amp;gt;
      ....(축약)
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;HTTP Status 404 &amp;ndash; Not Found&amp;lt;/h1&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이를 404.html 파일로 만들어서 웹에서 확인하면 아래와 같은 화면을 만날 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;185&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bU1QpI/dJMcaaRAb1Y/YmF02nD2WRYjU5cWMyjLkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bU1QpI/dJMcaaRAb1Y/YmF02nD2WRYjU5cWMyjLkk/img.png&quot; data-alt=&quot;404 Not Found&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bU1QpI/dJMcaaRAb1Y/YmF02nD2WRYjU5cWMyjLkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbU1QpI%2FdJMcaaRAb1Y%2FYmF02nD2WRYjU5cWMyjLkk%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;957&quot; height=&quot;185&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;185&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;404 Not Found&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;size18&quot;&gt;유효하지 않은 url으로 접근했을 때 나타나는 Whitelabel 페이지과 차이가 보입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mUrlk/dJMcafeh16J/VvhHTYanx56wTKK87IGKkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mUrlk/dJMcafeh16J/VvhHTYanx56wTKK87IGKkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mUrlk/dJMcafeh16J/VvhHTYanx56wTKK87IGKkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmUrlk%2FdJMcafeh16J%2FVvhHTYanx56wTKK87IGKkk%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;672&quot; height=&quot;301&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;301&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;분석&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위의 404 페이지를 생성하는 클래스는 &lt;b&gt;org.apache.catalina.valves.ErrorReportValve&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이는 Tomcat의 클래스이며 호출흐름을 간단히 요약해 보면 Tomcat 레벨에서 에러 페이지가 반환되었음을 추측할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1768633284500&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;요청
 &amp;darr;
DispatcherServlet 도달 ❌ (아예 못함)
 &amp;darr;
Tomcat 엔진 레벨
 &amp;darr;
ErrorReportValve
 &amp;darr;
기본 HTML 에러 페이지 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 해당 케이스의 경우 DispatcherServlet이나 Spring Filter 까지도 도달하지 못해서 적절한 로그가 남지 않고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 ErrorReportValve 클래스를 동일한 패키지명으로 재정의하고 에러 로그를 추가하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;849&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tRB5W/dJMcagdbPj2/9j1Ty66Hya3V6WEuwTXzo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tRB5W/dJMcagdbPj2/9j1Ty66Hya3V6WEuwTXzo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tRB5W/dJMcagdbPj2/9j1Ty66Hya3V6WEuwTXzo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtRB5W%2FdJMcagdbPj2%2F9j1Ty66Hya3V6WEuwTXzo1%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;1758&quot; height=&quot;849&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;849&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;511&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qa0WP/dJMcahQHLH7/brwPmHdyCCW3tnvNcmpWA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qa0WP/dJMcahQHLH7/brwPmHdyCCW3tnvNcmpWA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qa0WP/dJMcahQHLH7/brwPmHdyCCW3tnvNcmpWA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqa0WP%2FdJMcahQHLH7%2FbrwPmHdyCCW3tnvNcmpWA0%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;765&quot; height=&quot;511&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;511&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;size18&quot;&gt;이후에 확인해 보면 decodeUrl이 null 인 모습이 확인됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FmPEc/dJMcaa47cKk/zsD44XDdQpBmNhfvIhbln1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FmPEc/dJMcaa47cKk/zsD44XDdQpBmNhfvIhbln1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FmPEc/dJMcaa47cKk/zsD44XDdQpBmNhfvIhbln1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFmPEc%2FdJMcaa47cKk%2FzsD44XDdQpBmNhfvIhbln1%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;1516&quot; height=&quot;253&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;253&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;size18&quot;&gt;decodeUrl은 org.apache.catalina.connector.CoyoteAdapter 클래스의 postParseRequest 메서드에서 duplicate 메서드가 호출되면서 세팅됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;1204&quot; data-origin-height=&quot;579&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2HJlE/dJMcaiozMTA/vi9gqYtHI36L3r8PO8zSNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2HJlE/dJMcaiozMTA/vi9gqYtHI36L3r8PO8zSNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2HJlE/dJMcaiozMTA/vi9gqYtHI36L3r8PO8zSNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2HJlE%2FdJMcaiozMTA%2Fvi9gqYtHI36L3r8PO8zSNK%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;1204&quot; height=&quot;579&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;579&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;size18&quot;&gt;그리고 이때 if 구문에 만족하지 않는다면 else 구문으로 분기되며 in-memory 프로토콜 핸들러 로직으로 처리하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;소켓을 직접 타는 HTTP 요청이 아니라, 내부 컴포넌트 간에 메모리 상에서 전달된 요청으로 가정하여 decodeUrl에 값이 세팅되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 url에 null이 세팅된다면 org.apache.catalina.mapper.Mapper 클래스에서 Context를 초기화하지 않게 되고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;org.apache.catalina.core.StandardHostValve 클래스에 의해 404 상태코드가 세팅됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;521&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b35ThC/dJMcacom8bi/jgMZN3ZaHOE90uMFrgKp20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b35ThC/dJMcacom8bi/jgMZN3ZaHOE90uMFrgKp20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b35ThC/dJMcacom8bi/jgMZN3ZaHOE90uMFrgKp20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb35ThC%2FdJMcacom8bi%2FjgMZN3ZaHOE90uMFrgKp20%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;521&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;521&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉, 아래 분기문이 핵심인데 어떤 상황에서 getType을 호출했을 때 2가 나올 때도 있고 아닐 때도 있을까요?&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;} else if (undecodedURI.getType() == 2) {&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;size18&quot;&gt;톰캣은 내부적으로 org.apache.tomcat.util.buf.MessageBytes 클래스를 활용하는데 내부적으로 string인지 byte인지 chars 인지에 따라 타입을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그렇다면 이 Type이 언제 바뀔 수 있을까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pbItf/dJMcaia3Dgw/7VYO3lX9KJdR2SRqCE3O5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pbItf/dJMcaia3Dgw/7VYO3lX9KJdR2SRqCE3O5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pbItf/dJMcaia3Dgw/7VYO3lX9KJdR2SRqCE3O5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpbItf%2FdJMcaia3Dgw%2F7VYO3lX9KJdR2SRqCE3O5K%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;418&quot; height=&quot;500&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;toString 메서드가 호출될 때 내부적으로 type을 T_SRT로 변경합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이런 이유로 Request undecodedURI 필드에 디버깅 포인트를 걸고 getRequestURI() 메서드를 호출하게 되면 내부적으로 MessageBytes의 타입이 변경되게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvGKjz/dJMcabJMZZ9/mhc1nk9nE5xCzd6baI9Au1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvGKjz/dJMcabJMZZ9/mhc1nk9nE5xCzd6baI9Au1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvGKjz/dJMcabJMZZ9/mhc1nk9nE5xCzd6baI9Au1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvGKjz%2FdJMcabJMZZ9%2Fmhc1nk9nE5xCzd6baI9Au1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;933&quot; height=&quot;441&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;441&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 404가 발생할 수 있는 원인은 추측했는데 어디서 getRequestURI()가 호출되는지를 찾아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://medium.com/@wirelesser/debugging-spring-boot-application-health-check-fail-return-404-unhealthy-from-tomcat-b320863ea9ff&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;디버깅 시 헬스체크가 실패한다는 글&lt;/a&gt;을 참고해 보면 &lt;span style=&quot;background-color: #f2f2f2; color: #242424; text-align: start;&quot;&gt;open-telemetry-javaagent &lt;/span&gt;를 적용했을 때 requestURI&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #242424;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;의 toString을 호출하는 과정에서 &lt;span style=&quot;background-color: #f2f2f2; color: #242424; text-align: start;&quot;&gt;MessageBytes.toString() &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 호출된 것이 원인이라는 레퍼런스가 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후에 톰캣에서는 toString이 호출될 때 내부적으로 type을 변경하지 않도록 개선하였지만, 추후에 매번 toString이 호출되어 gc에 부담이 된다는 제보를 받고 내부적으로 type을 변경하여 toString을 호출하지 않도록 캐시하는 toStringType 메서드를 호출하도록 대체되면서 톰캣 버전에 따라 getRequestURI()를 호출한다면 내부적으로 MessageBytes의 type이 변경될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;유사하게 모니터링 시스템으로 pinpoint를 활용하고 있었기 때문에 pinpoint일 가능성도 열어두었지만 pinpoint가 에러를 유발한다면 간헐적으로 발생하는 것이 아니라 항상 발생해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 원인을 정확하게 파악하기 위해서 org.apache.catalina.connector.Request 클래스를 재정의하여 getRequestURI 메서드가 호출될 때 stack trace 로그를 남겨두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 stack trace 로그를 확인했을 때 원인은 애플리케이션의 버그였고, 코루틴 환경에서 다른 쓰레드에 의해 Request 객체의 getRequestURI() 메서드가 호출되었고 수많은 요청 중 간헐적으로 타이밍에 맞춰서 type이 변경되면 404 상태코드가 반환되고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Spring 에서는 RequestContextHolder를 통하여 ServletRequestAttributes 객체를 가져올 수 있고 이를 통하여 Request 객체에 접근할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 정보는 ThreadLocal 객체를 통해 접근되므로 코루틴에게 넘겨줄 때는 아래와 같이 넘겨줄 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769268493342&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun CoroutineContext.withRequestAttributesContext() : CoroutineContext{
    return this + RequestContextElement()
}

class RequestContextElement(
    private val requestAttributes: RequestAttributes? = RequestContextHolder.getRequestAttributes()
) : ThreadContextElement&amp;lt;RequestAttributes?&amp;gt; {

    companion object Key : CoroutineContext.Key&amp;lt;RequestContextElement&amp;gt;

    override val key: CoroutineContext.Key&amp;lt;RequestContextElement&amp;gt;
        get() = Key

    /**
     * 코루틴이 특정 스레드에서 실행되기 직전 호출
     */
    override fun updateThreadContext(context: CoroutineContext): RequestAttributes? {
        val previous = RequestContextHolder.getRequestAttributes()
        if (requestAttributes != null) {
            RequestContextHolder.setRequestAttributes(requestAttributes)
        } else {
            RequestContextHolder.resetRequestAttributes()
        }
        return previous
    }

    /**
     * 코루틴이 해당 스레드를 떠날 때 호출
     */
    override fun restoreThreadContext(
        context: CoroutineContext,
        oldState: RequestAttributes?
    ) {
        if (oldState != null) {
            RequestContextHolder.setRequestAttributes(oldState)
        } else {
            RequestContextHolder.resetRequestAttributes()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래와 같이 컨트롤러를 구성하고 성능테스트처럼 1초에 200번 정도 요청을 지속적으로 보내면 200 OK 대신 간헐적으로 404 응답을 받을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1769268583224&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @GetMapping(&quot;/404-test&quot;)
    fun hello(): String{
        CoroutineScope(Dispatchers.IO.withRequestAttributesContext()).launch {
            sleep(Random.nextLong(5, 500)) // 5 ~ 500 ms
            val servletRequestAttributes = RequestContextHolder.getRequestAttributes() as ServletRequestAttributes
            println(servletRequestAttributes.request.requestURI)
        }
        return &quot;hello&quot;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 간헐적으로 404 응답코드가 발생될 수 있는 원인을 분석했고, 실제 테스트로 재현도 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다만 서로 다른 요청의 Request 객체가 어떻게 공유되는거지? 라는 의문이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;org.apache.coyote.AbstractProtocol 클래스를 살펴보면 processorCache 라는 개념이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Processor를 매번 생성하지 않고 요청마다 cache에 push 하고 pop 하여 재사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Processor 내부에는 Request, Response 필드도 존재하며 매 요청이 끝나게 되면 recycle 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 과정에서 t1과 t2의 요청이 Reqeust 객체를 동시에 접근할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;수정 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;백그라운드 쓰레드에서 접근하는 Request 객체는 요청이 반환되는 순간 recycle 되어 내부적으로 null 값이 세팅되거나 다음 요청의 값으로 세팅될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 백그라운드 쓰레드에서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ServletRequestAttributes&lt;span&gt; 객체를 넘기고 사용하는 것은 개념상 적절하지 않습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;RequestContextHolder 대신 Spring Filter에서 RequestContextSnapshotHolder를 정의하여 requestURI 필드 등의 필요한 값들을 data class로 가지고 있으면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;재발 방지&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;백그라운드 쓰레드에 ServletRequestAttributes&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp; 객체를 넘겨서 활용하게 되면 recycle 되어 내부적으로 null 값이 세팅될 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;이때 requestURI() 메서드로 접근할 때 org.apache.catalina.connector.RequestFacade 객체가 request 객체가 null 인 경우 예외를 발생시킵니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;해당 예외를 우회하기 위해서는 org.apache.catalina.connector.Connector 클래스의 discardFacades 옵션을 비활성화해야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 옵션은 기본적으로 true인데 false로 비활성화하면 recycle이 발생할 때 Request 객체를 null으로 변경하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 해당 옵션을 false로 켜주어야 백그라운드 쓰레드에서 예외가 발생하지 않는데, Spring Application이 로드될 때 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;discardFacades&lt;span&gt; 옵션이 false 라면 예외를 발생시켜 실행되지 않도록 하여 재발방지까지 챙겨볼 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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://medium.com/@wirelesser/debugging-spring-boot-application-health-check-fail-return-404-unhealthy-from-tomcat-b320863ea9ff&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@wirelesser/debugging-spring-boot-application-health-check-fail-return-404-unhealthy-from-tomcat-b320863ea9ff&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring Framework</category>
      <author>Junuuu</author>
      <guid isPermaLink="true">https://junuuu.tistory.com/1048</guid>
      <comments>https://junuuu.tistory.com/1048#entry1048comment</comments>
      <pubDate>Sun, 25 Jan 2026 00:55:20 +0900</pubDate>
    </item>
    <item>
      <title>Apache Tomcat ErrorReportValve에 logOnError 옵션 추가하기</title>
      <link>https://junuuu.tistory.com/1047</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Apache&amp;nbsp;Tomcat의&amp;nbsp;ErrorReportValve는&amp;nbsp;HTTP&amp;nbsp;에러&amp;nbsp;발생&amp;nbsp;시&amp;nbsp;사용자에게&amp;nbsp;에러&amp;nbsp;페이지를&amp;nbsp;보여주는&amp;nbsp;역할을&amp;nbsp;합니다. &lt;br /&gt;하지만&amp;nbsp;에러가&amp;nbsp;발생했을&amp;nbsp;때&amp;nbsp;이를&amp;nbsp;로그로&amp;nbsp;남겨&amp;nbsp;빠르게&amp;nbsp;인지할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;기능은&amp;nbsp;제공하지&amp;nbsp;않았습니다. &lt;br /&gt;&lt;br /&gt;이로&amp;nbsp;인해&amp;nbsp;Tomcat&amp;nbsp;처리&amp;nbsp;단계에서&amp;nbsp;발생한&amp;nbsp;에러는&amp;nbsp;운영&amp;nbsp;환경에서&amp;nbsp;쉽게&amp;nbsp;놓치기&amp;nbsp;쉬웠고, &lt;br /&gt;이를&amp;nbsp;개선하기&amp;nbsp;위해&amp;nbsp;에러&amp;nbsp;로그를&amp;nbsp;남길&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;옵션을&amp;nbsp;추가하는&amp;nbsp;PR을&amp;nbsp;작성하게&amp;nbsp;되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Spring Boot + Tomcat 처리 흐름&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;754&quot; data-start=&quot;700&quot; data-ke-size=&quot;size18&quot;&gt;Spring Boot + Tomcat 환경에서 하나의 HTTP 요청은 보통 다음 순서로 처리됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1768999513995&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Client &amp;rarr; Tomcat &amp;rarr; Filter &amp;rarr; DispatcherServlet &amp;rarr; Controller&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;size18&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;여기서 DispatcherServlet이 요청을 받게 된다면 &lt;/span&gt;BasicErrorController가 오류를 처리하며, 기본 설정일 경우 우리가 잘 아는 &lt;b&gt;Whitelabel Error Page&lt;/b&gt;를 렌더링합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 만약 Tomcat이 처리하는 과정에서 오류가 발생하게 되면 요청은 &lt;b&gt;DispatcherServlet &lt;/b&gt;까지 도달하지 못하게 되며 이때는 ErrorReportValve 클래스에 의해 &lt;b&gt;Tomcat 기본 HTML 에러 페이지&lt;/b&gt;가 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예를 들면 규격에 맞지 않는 URI, 애플리케이션 버그로 인한 Tomcat 버그 유발 등이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉,&amp;nbsp;에러&amp;nbsp;페이지의&amp;nbsp;종류만&amp;nbsp;보아도&amp;nbsp;요청이&amp;nbsp;어느&amp;nbsp;단계에서&amp;nbsp;실패했는지를&amp;nbsp;유추할&amp;nbsp;수&amp;nbsp;있습니다. &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 에러는 발생하지만, 로그는 남지 않는다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;241&quot; data-start=&quot;54&quot; data-ke-size=&quot;size18&quot;&gt;ErrorReportValve가 처리한 에러는 애플리케이션 로그나 에러 로그에 별도로 남지 않아,&lt;br /&gt;문제 인지와 원인 분석이 외부 제보에 의존할 수밖에 없는 한계가 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;241&quot; data-start=&quot;54&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;241&quot; data-start=&quot;54&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;241&quot; data-start=&quot;54&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; Observability 개선 구현&lt;/b&gt;&lt;/h4&gt;
&lt;/div&gt;
&lt;p data-end=&quot;245&quot; data-start=&quot;50&quot; data-ke-size=&quot;size18&quot;&gt;ErrorReportValve에 logOnError 옵션을 추가하여, 에러 응답을 생성하는 시점에 요청 정보와 예외를 로그로 기록하도록 구현했습니다.&lt;/p&gt;
&lt;p data-end=&quot;245&quot; data-start=&quot;50&quot; data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;옵션이 활성화된 경우에만 로그를 남기도록 하여, 기존 동작과의 호환성을 유지하였으며, 이를 통해 Spring 이전 단계에서 발생한 에러도 운영 환경에서 즉시 인지할 수 있기를 기대합니다.&lt;/p&gt;
&lt;p data-end=&quot;245&quot; data-start=&quot;50&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;245&quot; data-start=&quot;50&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;PR 설명&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. logOnError 옵션 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기존에는&amp;nbsp;showReport,&amp;nbsp;showServerInfo&amp;nbsp;옵션만&amp;nbsp;존재했으며, &lt;br /&gt;여기에&amp;nbsp;에러&amp;nbsp;로깅&amp;nbsp;여부를&amp;nbsp;제어하는&amp;nbsp;logOnError&amp;nbsp;플래그를&amp;nbsp;추가하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본값을&amp;nbsp;false로&amp;nbsp;설정한&amp;nbsp;이유는&amp;nbsp;하위&amp;nbsp;호환성을&amp;nbsp;유지하기&amp;nbsp;위함이며, &lt;br /&gt;봇이나&amp;nbsp;비정상적인&amp;nbsp;요청으로&amp;nbsp;인해&amp;nbsp;불필요한&amp;nbsp;에러&amp;nbsp;로그가&amp;nbsp;대량으로&amp;nbsp;발생하는&amp;nbsp;경우를 &lt;br /&gt;운영&amp;nbsp;환경에서&amp;nbsp;제어할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;하기&amp;nbsp;위함입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 로깅 메시지 국제화 지원&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;톰캣은 LocalStrings.properties 파일에서 로깅 메시지의 국제화를 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;StringManager를 통하여 errorReportValve.errorLogged 특정 키로 조회하여 메시지를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 문서화 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;webapps/docs/config/valve.xml 문서에 logOnError 필드가 의미하는 바를 추가하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 테스트 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;TestErrorReportValve 클래스에&amp;nbsp; testLogOnErrorEnabled, testLogOnErrorDisabled 두 가지 테스트를 추가하여 옵션에 따라 에러로그가 남는지, 남지 않는지 검증하였습니다.&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;size18&quot;&gt;pr 링크&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- &lt;a href=&quot;https://github.com/apache/tomcat/pull/943&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/apache/tomcat/pull/943&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;993&quot; data-start=&quot;862&quot; data-ke-size=&quot;size18&quot;&gt;이번 logOnError 옵션 추가는 기능적으로는 작은 변화지만,&lt;br /&gt;운영 중 발생하는 Tomcat 레벨 에러를 인지하는 데에는 꽤 큰 차이를 만들어줍니다.&lt;br /&gt;비슷한 문제를 겪고 있다면 이 경험이 하나의 참고 사례가 되었으면 합니다.&lt;/p&gt;</description>
      <category>Spring Framework</category>
      <author>Junuuu</author>
      <guid isPermaLink="true">https://junuuu.tistory.com/1047</guid>
      <comments>https://junuuu.tistory.com/1047#entry1047comment</comments>
      <pubDate>Wed, 21 Jan 2026 22:13:09 +0900</pubDate>
    </item>
    <item>
      <title>Mongodb 쿼리 성능 개선하기 - 인덱스 추가</title>
      <link>https://junuuu.tistory.com/1046</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스를 운영하던 중 배치 서비스가 유독 느리게 수행되는 현상을 발견하게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문제의 원인을 분석하고 쿼리 성능을 개선하기 위해 인덱스를 적용했던 사례를 기록해보고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;문제 정의&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배치는 특정일자 전 데이터를 읽고, 해당 데이터가 특정 기간보다 오래되었다면 삭제하는 역할을 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그상 쿼리를 불러오고 삭제할 때 10초 정도의 시간이 소요되었음을 확인하여 해당 쿼리가 인덱스를 적절하게 활용하고 있지 않아서 데이터를 읽는 과정에서 느릴 것이라고 판단했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;쿼리 파악&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 쿼리는 requtestAt 기준으로 특정 날짜 이전의 데이터를 가져오고, type column으로 한번 더 equals 연산을 하여 데이터를 가져오고 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 정렬은 수행하지 않은 채로 100개의 데이터를 페이징 형식으로 가져오고 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 아래와 같은 쿼리로 데이터를 읽어오고 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1758986769909&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;db.my_collections.find({
    requestedAt: { $lt: ISODate(&quot;2025-08-28T00:00:00Z&quot;)},
    type: &quot;A&quot;
}).limit(100)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;실행 계획 분석&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1758987208286&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;db.my_collections.find({
    requestedAt: { $lt: ISODate(&quot;2025-08-28T00:00:00Z&quot;)},
    type: &quot;A&quot;
}).limit(100).explain(&quot;executionStats&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MongoDB의 explain() 메서드는 &lt;b&gt;쿼리가 어떻게 실행되는지에 대한 실행 계획(Execution Plan)&lt;/b&gt; 을 확인할 수 있게 해주는 도구입니다. 쿼리가 어떤 &lt;b&gt;인덱스&lt;/b&gt;를 사용하는지, 혹은 &lt;b&gt;컬렉션 스캔(Collection Scan = Full Scan)을&lt;/b&gt; 하는지 보여줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 메서드 파라미터에 &quot;executionStats&quot; 값을 넣어 실제 실행 결과에 대한 통계(스캔한 도큐먼트 수, 실행 시간 등)를 포함한 결과를 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1759052144442&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;executionStats: {
      &quot;nReturned&quot;: 100,
      &quot;executionTimeMillis&quot;: 7204,
      &quot;totalKeysExamined&quot;: 1268657,
      &quot;totalDocsExamined&quot;: 1268657,
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;executionStats 항목에서는 100건을 반환하려고 126만 건을 스캔해서 7초가 걸렸다는 사실을 알 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1759052334539&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;winningPlan: {
  &quot;stage&quot;: &quot;LIMIT&quot;,
  &quot;limitAmount&quot;: 100,
  &quot;inputStage&quot;: {
    &quot;stage&quot;: &quot;FETCH&quot;,
    &quot;filter&quot;: {
      &quot;type&quot;: { &quot;$eq&quot;: &quot;A&quot; }
    },
    &quot;inputStage&quot;: {
      &quot;stage&quot;: &quot;IXSCAN&quot;,
      &quot;keyPattern&quot;: { &quot;requestedAt&quot;: 1 },
      &quot;indexName&quot;: &quot;requestedAt_1&quot;,
      &quot;direction&quot;: &quot;forward&quot;,
      &quot;indexBounds&quot;: {
        &quot;requestedAt&quot;: [
          &quot;[new Date(-9223372036854775808), new Date(1756166400000))&quot;
        ]
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;winningPlan&lt;/b&gt; 은 쿼리 옵티마이저(Query Optimizer)가 여러 실행 계획 중에서 &lt;b&gt;최종적으로 선택한 실행 계획&lt;/b&gt;을 의미합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;87&quot; data-start=&quot;17&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;winningPlan 구조&lt;/b&gt;는 &lt;b&gt;트리 형태(계층 구조)로&lt;/b&gt; 표현되는데 &lt;b&gt;안쪽(inputStage)에 있는 stage일수록 먼저 실행&lt;/b&gt;되고, &lt;b&gt;바깥쪽 stage는 그 결과를 받아 처리&lt;/b&gt;하는 흐름입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 먼저 &lt;b&gt;IXSCAN&lt;/b&gt; 단계에서 특정 인덱스 필드(requestedAt)를 오름차순으로 전체 범위 스캔합니다. 이어서 &lt;b&gt;FETCH&lt;/b&gt; 단계에서 인덱스로 찾은 문서들을 실제 읽어오며 type = &quot;A&quot; 조건을 적용합니다. 마지막으로 &lt;b&gt;LIMIT&lt;/b&gt; 단계에서 그중 상위 100건만 반환합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 결과를 기반으로 requestedAt은 인덱스 스캔이 적절하게 동작했지만 해당 결과로 126만 건의 데이터가 후보군으로 나왔으며, 이후에 126만 건은 실제 문서를 읽어오면서 type이 A 인지 찾아오게 됩니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인덱스 추가&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1759053661064&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;db.collection.createIndex({ type: 1, requestedAt: -1 })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;성능을 개선하기 위해서 type을 먼저 두고, 그 안에서 requestedAt 정렬이 가능한 구조인 복합 인덱스를 추가할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MongoDB에서 복합 인덱스를 생성할때는 ESR rule에 따라 생성하는 것이 권장됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ESR 규칙은 복합 인덱스를 설계할 때 필드 순서를 정하는 가이드라인으로, Equality(동등성), Sort(정렬), Range(범위)의 약자입니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;동등성 조건으로 필터링한 후, 정렬 순서를 따르고 마지막으로 범위 연산을 적용하여 인덱스를 효율적으로 활용할 수 있도록 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MongoDB 4.2 이상은 기본적으로 백그라운드 인덱스 생성(background: true)을 지원해서 DB 전체 write는 막지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인덱스를 만드는 동안 기존 데이터, 새롭게 생성되는 데이터를 B-Tree에 넣어주어야 해서 CPU, 메모리, I/O를 많이 활용하여 latency가 늘어날 수 있어서 모니터링이 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인덱스 생성 이후&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1759062913415&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;executionStats: {
      &quot;nReturned&quot;: 100,
      &quot;executionTimeMillis&quot;: 8,
      &quot;totalKeysExamined&quot;: 100,
      &quot;totalDocsExamined&quot;: 100,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;totalKeysExamined는 스캔한 인덱스 항목수인데 백만건 -&amp;gt; 백건으로 줄어든 모습을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실행 시간도 7204ms에서 8ms로 감소하였습니다. (약 900배)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1759063561153&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;winningPlan&quot;: {
  &quot;stage&quot;: &quot;LIMIT&quot;,
  &quot;limitAmount&quot;: 100,
  &quot;inputStage&quot;: {
    &quot;stage&quot;: &quot;FETCH&quot;,
    &quot;inputStage&quot;: {
      &quot;stage&quot;: &quot;IXSCAN&quot;,
      &quot;keyPattern&quot;: {
        &quot;type&quot;: 1,
        &quot;requestedAt&quot;: -1
      },
      &quot;indexName&quot;: &quot;type_1_requestedAt_-1&quot;,
      &quot;isMultiKey&quot;: false,
      &quot;multiKeyPaths&quot;: {
        &quot;serviceType&quot;: [],
        &quot;requestedAt&quot;: []
      },
      &quot;isUnique&quot;: false,
      &quot;isSparse&quot;: false,
      &quot;isPartial&quot;: false,
      &quot;indexVersion&quot;: 2,
      &quot;direction&quot;: &quot;forward&quot;,
      &quot;indexBounds&quot;: {
        &quot;type&quot;: [
          &quot;[\&quot;A\&quot;, \&quot;A\&quot;]&quot;
        ],
        &quot;requestedAt&quot;: [
          &quot;(new Date(1758844800000), new Date(-9223372036854775808)]&quot;
        ]
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;type-1_requestedAt_-1 복합 인덱스를 활용하여 IXSCAN을 수행한 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고자료&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://www.mongodb.com/ko-kr/docs/manual/reference/explain-results/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.mongodb.com/ko-kr/docs/manual/reference/explain-results/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://www.mongodb.com/ko-kr/docs/manual/reference/method/db.collection.explain/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.mongodb.com/ko-kr/docs/manual/reference/method/db.collection.explain/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트/mongoDB</category>
      <author>Junuuu</author>
      <guid isPermaLink="true">https://junuuu.tistory.com/1046</guid>
      <comments>https://junuuu.tistory.com/1046#entry1046comment</comments>
      <pubDate>Sun, 28 Sep 2025 21:48:36 +0900</pubDate>
    </item>
    <item>
      <title>개발환경의 파티션은 어떤 consumer가 점유했을까?</title>
      <link>https://junuuu.tistory.com/1045</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 kafka의 파티션이 3개이고, consumer가 1개라면 어떤 consumer가 파티션을 점유할까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 kafka의 파티션이 3개이고, consumer가 2개(로컬환경, 개발환경)라면 어떤 consumer가 파티션을 점유할까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추가로 concurrency 옵션은 어떤 관계가 있을까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 질문에 답을 알아가는 과정을 통하여 예측가능한 테스트 환경을 만들 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Kafka Topic과 Partition&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1046&quot; data-origin-height=&quot;511&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nlavO/btsOXkBFUpx/qdfrxWghw75hTlbsRPHv3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nlavO/btsOXkBFUpx/qdfrxWghw75hTlbsRPHv3K/img.png&quot; data-alt=&quot;https://www.lydtechconsulting.com/blog-kafka-message-keys.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nlavO/btsOXkBFUpx/qdfrxWghw75hTlbsRPHv3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnlavO%2FbtsOXkBFUpx%2FqdfrxWghw75hTlbsRPHv3K%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;1046&quot; height=&quot;511&quot; data-origin-width=&quot;1046&quot; data-origin-height=&quot;511&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.lydtechconsulting.com/blog-kafka-message-keys.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Kafka는 이벤트의 관심사에 따라 각각 topic에 메시지를 적재합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;foo라는 topic이 구성되어 있고 3개의 파티션으로 구성되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 파티션에는 kafka consumer가 붙어서 메시지를 처리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 구조 덕분에 consumer의 처리량을 늘리고 싶다면 파티션을 증설하고 kafka consumer를 매핑시킬 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 그림에서 파티션을 1개 더 늘리게 되면 유휴 kafka consumer 1대가 메시지를 consume 하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;파티션이 3개, consumer가 1개일 때 ( &lt;span style=&quot;text-align: start;&quot;&gt;concurrency&amp;nbsp;&lt;/span&gt; =1 )&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;433&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vTaJv/btsOXSYYYhj/gCBgnA7zT3c9f8YgK7Qb40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vTaJv/btsOXSYYYhj/gCBgnA7zT3c9f8YgK7Qb40/img.png&quot; data-alt=&quot;https://www.lydtechconsulting.com/blog-kafka-message-keys.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vTaJv/btsOXSYYYhj/gCBgnA7zT3c9f8YgK7Qb40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvTaJv%2FbtsOXSYYYhj%2FgCBgnA7zT3c9f8YgK7Qb40%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;1044&quot; height=&quot;433&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;433&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.lydtechconsulting.com/blog-kafka-message-keys.html&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;concurrency&amp;nbsp;옵션은&amp;nbsp;애플리케이션&amp;nbsp;인스턴스&amp;nbsp;내에서&amp;nbsp;동시에&amp;nbsp;실행할&amp;nbsp;KafkaConsumer&amp;nbsp;수를&amp;nbsp;의미합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본적으로 1로 할당되며 만약 concurrency를 3으로 할당하면 각각의 consumer가 파티션을 1개씩 맡아 병렬로 처리하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 상황을 개발 환경의 consumer로 대입해서 생각해 보면 개발 환경의 consumer 1개가 파티션 3개의 메시지를 잘 처리하고 있을 겁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;파티션이 3개이고, consumer가 2개일 때(concurrency = 1)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&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;695&quot; data-origin-height=&quot;869&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxwLzr/btsOXltONo5/VQ0gvPgc2weLvQ2q2h7UBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxwLzr/btsOXltONo5/VQ0gvPgc2weLvQ2q2h7UBK/img.png&quot; data-alt=&quot;https://developer.confluent.io/courses/architecture/consumer-group-protocol/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxwLzr/btsOXltONo5/VQ0gvPgc2weLvQ2q2h7UBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxwLzr%2FbtsOXltONo5%2FVQ0gvPgc2weLvQ2q2h7UBK%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;500&quot; height=&quot;625&quot; data-origin-width=&quot;695&quot; data-origin-height=&quot;869&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.confluent.io/courses/architecture/consumer-group-protocol/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;consumer 2를 개발환경이라고 생각하고, 로컬환경에서 consumer1을 구동했다고 가정해 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그런 경우 위 그림처럼 파티션 3개가 2개, 1개로 consumer에게 할당되게 됩니다. (range assignor라고 가정)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 로컬환경의 consumer1에 파티션이 2개가 할당되고, 개발환경의 consumer 2에 파티션에 1개가 할당되었다면 로컬환경에서 consumer1을 테스트해보고 싶은데 topic에 메시지를 발행했을 때 파티션 3으로 발행된다면 개발환경의 consumer2 가 동작하게 되며 의도한 테스트를 할 수 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;왜 이런 일이 발생하는 걸까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Range Assignor 동작과정&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; range assignor의 동작과정을 이해하면 위의 상황을 이해할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;kafka에서는 여러개의 consumer가 존재할 때 Partition Assignor를 통해 consumer가 어떤 파티션을 처리할지 정해집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 별다른 설정이 없다면 기본적으로 range assignor가 활용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;range assignor는 consumer id를 사전순으로 정렬하고 파티션을 균등하게 범위를 나누어 consumer에 할당합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이때 파티션 수를 소비자 수로 나누고 균등하게 분배합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;922&quot; data-start=&quot;887&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;총 파티션 수 = 3, Consumer 수 = 2 인경우&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1042&quot; data-start=&quot;965&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1000&quot; data-start=&quot;965&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Consumer 당 기본 할당 개수 = 3 / 2 = 1&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1042&quot; data-start=&quot;1004&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;남는 파티션: 3 % 2 = 1 &amp;rarr; 사전순 앞 consumer가 추가 1개 더 가짐&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;분배 결과&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1270&quot; data-start=&quot;1066&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;1270&quot; data-start=&quot;1163&quot;&gt;
&lt;tr data-end=&quot;1217&quot; data-start=&quot;1163&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1185&quot; data-start=&quot;1163&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;consumer-A&lt;/span&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1217&quot; data-start=&quot;1185&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-0, partition-1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1270&quot; data-start=&quot;1218&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1240&quot; data-start=&quot;1218&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;consumer-B&lt;/span&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1270&quot; data-start=&quot;1240&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-2&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;총 파티션 수 = 5, Consumer 수 = 3 인경우&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Consumer 당 기본 할당 개수 = 5 / 3 = 1&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;584&quot; data-start=&quot;569&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;남는 파티션: 5 % 3 = 2개 &amp;rarr; 앞에서부터 한 개씩 추가로 할당 &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;651&quot; data-start=&quot;586&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;분배 결과&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;consumer-A&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2개&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-0, partition-1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;consumer-B&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2개&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-2, partition-3&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;consumer-C&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1개&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-4&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;파티션이 5개이고, consumer가 2개일 때(concurrency = 5)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt; concurrency&amp;nbsp;옵션은&amp;nbsp;애플리케이션&amp;nbsp;인스턴스&amp;nbsp;내에서&amp;nbsp;동시에&amp;nbsp;실행할&amp;nbsp;KafkaConsumer&amp;nbsp;수를&amp;nbsp;의미합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;concurrency를 5로 설정하게 되면 KafkaConsumer 수가 5개가 되고 consumer id도 5개가 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;consumer A의 concurrency가 5이고, consumer B의 concurrency는 1일 때&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;506&quot; data-start=&quot;489&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;총 Consumer 수&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;604&quot; data-start=&quot;508&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;559&quot; data-start=&quot;508&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;App A: 5개 (appA-consumer-0 ~ appA-consumer-4)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;591&quot; data-start=&quot;560&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;App B: 1개 (appB-consumer-0)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;604&quot; data-start=&quot;592&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;rarr; &lt;b&gt;총 6개&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;파티션 수 = 5&lt;/b&gt;,&amp;nbsp;&lt;b&gt;Consumer 수 = 6&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;780&quot; data-start=&quot;706&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;756&quot; data-start=&quot;706&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;rarr; 5 / 6 = 0 &amp;rarr; 파티션 5개 모두&amp;nbsp;&lt;b&gt;사전순 상위 5명에게 1개씩 할당&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;780&quot; data-start=&quot;760&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;하위 1명은 idle 상태&lt;/b&gt;&lt;/span&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; consumer A의 concurrency가 5이고, consumer B의 concurrency는 1일 때 &lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;appA-consumer-0&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-0&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;appA-consumer-1&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;appA-consumer-2&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-2&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;appA-consumer-3&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-3&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;appA-consumer-4&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-4&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;appB-consumer-0&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;없음 (idle)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 파티션을 App A가 점유하고, App B는 메시지를 전혀 소비하지 않습니다.&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; consumer A의 concurrency가 1이고, consumer B의 concurrency는 5일 때&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;appA-consumer-0&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-0&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;appB-consumer-0&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;appB-consumer-1&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-2&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;appB-consumer-2&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-3&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;appB-consumer-3&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;partition-4&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;appB-consumer-4&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;없음 (idle)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;총&amp;nbsp;5개의&amp;nbsp;파티션이&amp;nbsp;앞에서부터&amp;nbsp;1개씩&amp;nbsp;차례로&amp;nbsp;할당되며,&amp;nbsp;마지막&amp;nbsp;consumer&amp;nbsp;1명은&amp;nbsp;할당을&amp;nbsp;못&amp;nbsp;받아&amp;nbsp;idle&amp;nbsp;상태가&amp;nbsp;됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; &lt;span style=&quot;text-align: start;&quot;&gt;예측 가능한 테스트 환경 만들기&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;range assignor의 동작과정을 학습했으니 위 문제를 해결해볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로컬환경의 Consumer ID는 의도적으로 prefix에 00을 넣어주어 사전순으로 당기고 concurrency를 파티션 개수보다 많게 설정하게 되면 로컬환경의 consumer가 실행될 때 리밸런싱이 일어나게 되고 range assignor에 의해 모든 파티션을 가져와 로컬환경의 consumer가 처리하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 새롭게 개발한 로직으로 메시지가 consume 되어 예측가능한 테스트 환경을 만들어낼 수 있습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>프로젝트/kafka</category>
      <author>Junuuu</author>
      <guid isPermaLink="true">https://junuuu.tistory.com/1045</guid>
      <comments>https://junuuu.tistory.com/1045#entry1045comment</comments>
      <pubDate>Mon, 30 Jun 2025 01:10:01 +0900</pubDate>
    </item>
    <item>
      <title>NoSuchFieldError 원인 - gradle dependencies 문제 파악하기</title>
      <link>https://junuuu.tistory.com/1044</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;라이브러리 버전 업그레이드를 진행하던 중 NoSuchFieldError 예외가 발생했습니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;이 글에서는 NoSuchFieldError가 어떤 상황에서 발생하는지, 실제 발생 사례를 바탕으로 원인 분석 및 해결 과정을 정리해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✅ NoSuchFieldError란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;879&quot; data-start=&quot;794&quot; data-ke-size=&quot;size18&quot;&gt;NoSuchFieldError는 Java 애플리케이션 실행 중 특정 클래스에서 존재하지 않는 필드에 접근할 때 발생하는 &lt;b&gt;런타임 에러&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;879&quot; data-start=&quot;794&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1050&quot; data-start=&quot;881&quot; data-ke-size=&quot;size18&quot;&gt;컴파일 시점에는 정상적으로 컴파일되었지만, 런타임 시점에 클래스 간 버전이 맞지 않아 필드가 존재하지 않는 경우 주로 발생합니다.&lt;/p&gt;
&lt;p data-end=&quot;1050&quot; data-start=&quot;881&quot; data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;예를 들어, 어떤 라이브러리의 enum 클래스에 필드가 추가되었지만, 실제 실행 시에는 해당 필드가 없는 구버전의 enum이 로드될 경우 이 에러가 발생할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;맥락에 대해 추가로 설명하자면 boot2, boot3 gradle dependencies가 존재했고 해당 프로젝트는 boot2 기반의 프로젝트였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;코드의 수정의 최소화하며 boot2 -&amp;gt; boot3로 마이그레이션을 수월하게 하기 위해 동일한 패키지명과 이름을 가진&lt;span&gt;&amp;nbsp;&lt;/span&gt;Class들이&lt;span&gt;&amp;nbsp;&lt;/span&gt;2개씩 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이때 의존성을 올리는 과정에서 특정 Class 에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;NoSuchFieldError가 발생했으므로 컴파일러에 의해 의도되지 않은 Class가 로드되었을 가능성을 의심했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;라이브러리 의존성 분석&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래와 같은 명령어로 gradle dependencies 의존성 tree 분석이 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749355066930&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew :submodule-name:dependencies --configuration runtimeClasspath&lt;/code&gt;&lt;/pre&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;1030&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WGIIl/btsOsuZmbbx/fBfKUMF4aMhwpbhXpbdLgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WGIIl/btsOsuZmbbx/fBfKUMF4aMhwpbhXpbdLgK/img.png&quot; data-alt=&quot;jpa-paging 이라는 서브모듈의 의존성 트리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WGIIl/btsOsuZmbbx/fBfKUMF4aMhwpbhXpbdLgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWGIIl%2FbtsOsuZmbbx%2FfBfKUMF4aMhwpbhXpbdLgK%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;1030&quot; height=&quot;563&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;563&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;jpa-paging 이라는 서브모듈의 의존성 트리&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;size18&quot;&gt;이를 통해 하위 라이브러리가 어떤 의존성을 가지고 있는지 분석할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실제 의존성 분석을 수행했을 때 A 라는 enum class가 boot2, boot3 버전 모두가 의존성으로 가져와졌고 boot2 버전의 enum class가 컴파일되었지만, 실제 런타임에서 특정 모듈은 boot3 버전의 enum class를 의존하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;boot3 버전의 enum class에는 C라는 필드가 존재했지만 boot2 버전의 enum class에는 C라는 필드가 존재하지 않아 실제 런타임에 접근하려다 보니 NoSuchFieldError가 발생하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Gradle이 의존성을 선택하는 과정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;같은 group:artifacte에서 충돌이 나면 기본적으로 더 높은 버전을 선택합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749356097278&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;io.micrometer:micrometer-observation:1.10.7 -&amp;gt; 1.11.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1784&quot; data-start=&quot;1754&quot; data-ke-size=&quot;size18&quot;&gt;하지만 다음과 같은 경우에는 충돌이 해소되지 않습니다:&lt;/p&gt;
&lt;pre id=&quot;code_1749356120655&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;io.micrometer.boot2:micrometer-observation
io.micrometer.boot3:micrometer-observation&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 두 모듈에서 A라는 enum class를 동일하게 가지는 경우는 어떻게 될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 JVM 클래스 로딩 과정에서 순서에 따라 하나의 클래스가 선택되며, 이는 우리가 의도한 버전이 아닐 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 케이스는 boot2 프로젝트에서 boot3 모듈을 참조하려다가 발생한 문제여서 boot2 모듈을 의존하도록 변경하면 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;특정 상황에서는 활용하지 않는 모듈을 의도적으로 exclude 하여 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한 이 경우 런타임에서 감지되기 때문에 테스트코드로 커버리지가 되는 경우라면 빠르게 탐지될 수 있습니다.&lt;/p&gt;</description>
      <author>Junuuu</author>
      <guid isPermaLink="true">https://junuuu.tistory.com/1044</guid>
      <comments>https://junuuu.tistory.com/1044#entry1044comment</comments>
      <pubDate>Sun, 8 Jun 2025 13:18:12 +0900</pubDate>
    </item>
    <item>
      <title>Kotlin coroutine graceful shutdown in spring boot</title>
      <link>https://junuuu.tistory.com/1043</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;graceful shutdown&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;graceful shutdown은 애플리케이션이 종료될 때, 현재 진행 중인 작업을 완료한 후 종료하는 것을 말합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;couroutine과 graceful shutdown&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1743906129025&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class CoroutineGracefulShutdown() : ApplicationRunner {
    override fun run(args: ApplicationArguments) {        
        runBlocking {
            launch(Dispatchers.IO) {
                logger.info { &quot;start&quot; }
                sleep(30000) // 30초 기다림
                logger.info { &quot;end&quot; }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 위와 같은 코드를 실행했다가 intellij의 Stop 버튼을 눌러서 SIGINT로 애플리케이션의 종료를 수행하면 어떻게 될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;start 요청이 보이고 30초를 기다리지 않고 애플리케이션이 종료되어 end 로그가 남지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 해당 coroutine이 애플리케이션에서 데이터베이스에 적재하는 등 중요한 역할을 수행하고 있다면 데이터 정합성이 맞지 않게 되고, 배포과정에서 간헐적으로 발생하기 때문에 이 원인을 찾기 어려울 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Spring이 지원해 주는 option&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1743906368814&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  shutdown: graceful // 기본 값은 immediate&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;size18&quot;&gt;위 옵션을 적용하더라도 ApplicationRunner의 경우에는 Tomcat의 요청을 받고 connection이 수립되지 않기 때문에 단순하게 위 옵션으로는 coroutine의 graceful shtudown이 지원되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;coroutine의 CoroutineDispatcher&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;CoroutineDispatcher는 만들어진 코루틴을 스레드로 보내는 역할을 수행하는 객체입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743912991089&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;launch(Dispatchers.IO) // &amp;lt;&amp;lt; CoroutineDispatcher&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;launch에서는 CoroutineContext를 인자로 받아 넘기게 되는데 이때 CoroutineDispatcher도 CoroutineContext를 구현하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 코드에서는 Dispatchers.IO가 코루틴을 쓰레드로 보내는 역할을 수행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ThreadPool의 graceful shutdown&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;kotlin의 coroutine도 결국에는 쓰레드를 점유해서 수행되다 보니 수행되는 스레드가 graceful shutdown 된다면 coroutine도 자연스럽게 graceful shutdown이 지원될 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Spring에서 ThreadPool의 graceful shutdown을 지원하기 위해서는 아래와 같이 ThreadPool을 선언할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743913398759&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class MyThreadPool{
    @Bean
    fun myExecutor(): ThreadPoolTaskExecutor{
        val executor = ThreadPoolTaskExecutor()
        executor.corePoolSize = 10
        executor.maxPoolSize = 10
        executor.queueCapacity = 0
        executor.setThreadNamePrefix(&quot;test-task-&quot;)
        executor.setWaitForTasksToCompleteOnShutdown(true)
        executor.setAwaitTerminationSeconds(40)
        executor.initialize()
        return executor
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 코드는 40초의 유예시간을 주어 thread pool 내의 task가 종료될 수 있는 시간을 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단, ThreadPoolTaskExecutor를 Spring Bean으로 등록하지 않는다면 graceful shutdown이 지원되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Executor와 CoroutineDispatcher&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 ThreadPoolTaskExecutor는 Executor 인터페이스를 구현하고 있으며, launch에 인자로 넘기기 위해서는 CoroutineDispatcher 객체를 넘겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;kotlin coroutine 라이브러리에는 아래 코드와 같이 Executor를 CoroutineDispatcher로 변환하는 확장함수가 제공되는데 이를 활용해 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743913546526&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Executor.asCoroutineDispatcher()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;최종 코드&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1743913762198&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class CoroutineGracefulShutdown(
    private val myExecutor: ThreadPoolTaskExecutor,
) : ApplicationRunner {
    override fun run(args: ApplicationArguments?) {
        val coroutineDispatcher = myExecutor.asCoroutineDispatcher()

        runBlocking {
            launch(coroutineDispatcher) {
                logger.info { &quot;start&quot; }
                sleep(30000) // 30초 기다림
                logger.info { &quot;end&quot; }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;graceful shutdown이 지원되도록 옵션을 설정한 ThreadPoolTaskExecutor 객체를 주입받아서 CoroutineDispatcher로 변환한 후 넘겨주게 되면 intellij Stop으로 SIGINT 요청을 날리면 30초를 기다리고 end 로그까지 출력되는 모습을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://junuuu.tistory.com/885&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Boot Graceful shtudown 동작과정&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743913347243&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;Spring Boot Graceful shutdown 동작과정&quot; data-og-description=&quot;개요 Spring Boot Application에서 Controller가 요청을 처리하고 응답이 되지 않았는데 종료요청이 도달하면 어떻게 될까요? Client는 응답을 받지 못하고 timeout이 발생합니다. Spring Boot 2.3에서 제공하는 gr&quot; data-og-host=&quot;junuuu.tistory.com&quot; data-og-source-url=&quot;https://junuuu.tistory.com/885&quot; data-og-url=&quot;https://junuuu.tistory.com/885&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bbxiHy/hyYA7TsyDY/8fcKusKKMTMUYZtjYIyVYk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/GmsTE/hyYChumPP8/lvl64nIkigKunMinHZ9ozk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://junuuu.tistory.com/885&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://junuuu.tistory.com/885&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bbxiHy/hyYA7TsyDY/8fcKusKKMTMUYZtjYIyVYk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/GmsTE/hyYChumPP8/lvl64nIkigKunMinHZ9ozk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot Graceful shutdown 동작과정&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개요 Spring Boot Application에서 Controller가 요청을 처리하고 응답이 되지 않았는데 종료요청이 도달하면 어떻게 될까요? Client는 응답을 받지 못하고 timeout이 발생합니다. Spring Boot 2.3에서 제공하는 gr&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;junuuu.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://kotlinworld.com/141&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CoroutineDispatcher란 무엇인가?&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743913347992&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;[Coroutine] 3.  CoroutineDispatcher 란 무엇인가?&quot; data-og-description=&quot;Coroutine을 공부하면서 CoroutineDispatcher에 대해 상세히 설명된 글이 없어서 이 글을 작성하게 되었다. 많은 사람들에게 도움이 되길 바란다. CoroutineDispatcher 란 무엇인가? 코루틴을 시작하게 되면, Co&quot; data-og-host=&quot;kotlinworld.com&quot; data-og-source-url=&quot;https://kotlinworld.com/141&quot; data-og-url=&quot;https://kotlinworld.com/141&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/szp0P/hyYA9wYp3F/B5lIGSsvZY1mhzEQc01ITK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/boF8KQ/hyYBfxcoeI/3damtx5gMp5qsTImMUFyXK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bmpYIT/hyYBh2vZpz/FWXNSomnQNk2ZtfvZlNk41/img.png?width=978&amp;amp;height=595&amp;amp;face=0_0_978_595&quot;&gt;&lt;a href=&quot;https://kotlinworld.com/141&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kotlinworld.com/141&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/szp0P/hyYA9wYp3F/B5lIGSsvZY1mhzEQc01ITK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/boF8KQ/hyYBfxcoeI/3damtx5gMp5qsTImMUFyXK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bmpYIT/hyYBh2vZpz/FWXNSomnQNk2ZtfvZlNk41/img.png?width=978&amp;amp;height=595&amp;amp;face=0_0_978_595');&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;[Coroutine] 3. CoroutineDispatcher 란 무엇인가?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Coroutine을 공부하면서 CoroutineDispatcher에 대해 상세히 설명된 글이 없어서 이 글을 작성하게 되었다. 많은 사람들에게 도움이 되길 바란다. CoroutineDispatcher 란 무엇인가? 코루틴을 시작하게 되면, Co&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kotlinworld.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://junuuu.tistory.com/873&quot;&gt;ThreadPoolTaskExecutor란?&amp;nbsp;&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743913347512&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;ThreadPoolTaskExecutor란? (ThreadPoolTaskExecutor vs ThreadPoolExecutor)&quot; data-og-description=&quot;ThreadPoolTaskExecutor란? public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport implements AsyncListenableTaskExecutor, SchedulingTaskExecutor { private final Object poolSizeMonitor = new Object(); private int corePoolSize = 1; private &quot; data-og-host=&quot;junuuu.tistory.com&quot; data-og-source-url=&quot;https://junuuu.tistory.com/873&quot; data-og-url=&quot;https://junuuu.tistory.com/873&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RZJDv/hyYCgoG4KX/oeg4VpofG6kJWvzyUadGJ0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/jEsMb/hyYBbatdM5/yv1694eyiukFSKU7FvKrM1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://junuuu.tistory.com/873&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://junuuu.tistory.com/873&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RZJDv/hyYCgoG4KX/oeg4VpofG6kJWvzyUadGJ0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/jEsMb/hyYBbatdM5/yv1694eyiukFSKU7FvKrM1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;ThreadPoolTaskExecutor란? (ThreadPoolTaskExecutor vs ThreadPoolExecutor)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ThreadPoolTaskExecutor란? public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport implements AsyncListenableTaskExecutor, SchedulingTaskExecutor { private final Object poolSizeMonitor = new Object(); private int corePoolSize = 1; private&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;junuuu.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring Framework</category>
      <author>Junuuu</author>
      <guid isPermaLink="true">https://junuuu.tistory.com/1043</guid>
      <comments>https://junuuu.tistory.com/1043#entry1043comment</comments>
      <pubDate>Sun, 6 Apr 2025 13:31:22 +0900</pubDate>
    </item>
    <item>
      <title>humongous object java</title>
      <link>https://junuuu.tistory.com/1042</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;humongous object 란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;humongous는 거대한 이라는 사전적 의미를 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;humongous object는 이 글에서는 앞으로 거대한 객체로 칭하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 거대하다는 것을 추상적인 의미를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;누군가는 1KB 정도의 객체를 거대하다고 생각할 수 있고, 누군가는 1MB 정도가 거대하다고 생각할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;JVM 세상에서 거대한 객체로 인식되는 것은 GC와 연관 있기 때문에 GC에 대해서도 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JVM GC - G1GC&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2812&quot; data-origin-height=&quot;1555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwd1Wo/btsMKSn6pva/zER2xYhDkyWWavANyOSDek/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwd1Wo/btsMKSn6pva/zER2xYhDkyWWavANyOSDek/img.jpg&quot; data-alt=&quot;https://taes-k.github.io/2021/09/05/g1gc-detail/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwd1Wo/btsMKSn6pva/zER2xYhDkyWWavANyOSDek/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwd1Wo%2FbtsMKSn6pva%2FzER2xYhDkyWWavANyOSDek%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;2812&quot; height=&quot;1555&quot; data-origin-width=&quot;2812&quot; data-origin-height=&quot;1555&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://taes-k.github.io/2021/09/05/g1gc-detail/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;JDK 11부터는 default GC가 G1GC입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;G1GC의 경우 Heap이 동일한 크기의 여러 지역으로 분할되어 있는 것이 특징입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;각 지역은 Eden, Survior, Tenured로 분류되며 새로운 객체가 생성되면 Eden으로 할당됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 GC 사이클에서 한 번 이상 살아남은 객체는 Survior로 이동하고, 충분히 GC 사이클에서 살아남으면 Tenured 지역으로 이동됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이때 한 가지 예외로 객체가 거대한 객체로 분류되는 경우에는 바로 Tenured 지역으로 할당됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;거대한 객체의 기준&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;G1GC에서 지역의 메모리 영역 50%보다 큰 모든 객체를 거대한 객체로 간주합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 거대한 객체가 한 영역보다 작으면 전체 영역을 차지하고, N개의 영역보다 크고 N+1 개의 영역보다 작으면 N+1개의 영역을 차지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예를 들어 32MB 지역을 가진다면 객체가 16MB가 넘는 경우 거대한 객체로 분류됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;G1GC의 하나의 메모리 영역은 -XX:G1HeapRegionSize의 옵션으로 설정될 수 있으며 기본적으로는 최대 Heap 사이즈의 1/2048 만큼의 계산된 사이즈를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 옵션을 주어 설정한다면 1 ~ 32MB 정도로 설정할 수 있으며 2의 거듭제곱 값으로 주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; G1HeapRegionSize 확인 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvEVfW/btsMMA7wuth/0KuWDg6GkUnHj4K0LrFOqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvEVfW/btsMMA7wuth/0KuWDg6GkUnHj4K0LrFOqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvEVfW/btsMMA7wuth/0KuWDg6GkUnHj4K0LrFOqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvEVfW%2FbtsMMA7wuth%2F0KuWDg6GkUnHj4K0LrFOqK%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;778&quot; height=&quot;398&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;jcmd란 JVM 프로세스의 상태를 진단하고&amp;nbsp; 모니터링하기 위한 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;814&quot; data-origin-height=&quot;573&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FKHsG/btsMLIemTjO/KQo9aJxYAcPGHp5tYAkD61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FKHsG/btsMLIemTjO/KQo9aJxYAcPGHp5tYAkD61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FKHsG/btsMLIemTjO/KQo9aJxYAcPGHp5tYAkD61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFKHsG%2FbtsMLIemTjO%2FKQo9aJxYAcPGHp5tYAkD61%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;763&quot; height=&quot;537&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;573&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;jcmd 명령어를 통하여 PID를 탐색하고 VM.flags (현재 설정된 옵션 목록 출력)을 통하여 G1 HeapRegionSize크기를 확인해 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;거대한 객체가 Java Heap에서 가지는 의미&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GC가 발생하게 되면 활용하지 않는 영역은 제거하고, 각 객체들을 영역별로 옮기는 과정에서 조각 모음의 역할도 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단, 거대한 객체의 경우 회수되기 전까지 잉여 공간을 활용할 수 없어 메모리 파편화가 발생하고, 공간이 충분한데도 메모리가 부족하여 OOM이 발생할 수 있습니다.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;또한 Minor GC의 경우 Eden 영역의 객체를 Survivor 영역으로 이동시키며 속도가 수십 ms 정도로 빠릅니다. (Survivor 영역에서 오래 살아남은 객체를 Tenured 영역으로 승격시키기도 합니다)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;하지만 거대한 객체는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Tenured&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;영역으로 바로 할당되므로 Full GC가 발생할 가능성을 증가시킵니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;Full GC의 경우 수백ms 에서 수초가 수행될 수 있어서 애플리케이션의 응답속도가 느려질 수 있게 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;거대한 객체가 탐지되었을 때 대응 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 데이터를 Chunking 할 수 있다면 여러 개의 작은 List로 분할해 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. Snappy, LZ4, GZIP 등을 활용하여 압축을 활용해볼 수 있습니다. (단, 압축으로 인하여 CPU 오버헤드가 생길 수 있습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. -XX:G1HeapRegionSize 옵션을 통해 Region 크기를 증가시켜 거대한 객체로 포함되는 기준을 높일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://devblogs.microsoft.com/java/whats-the-deal-with-humongous-objects-in-java/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devblogs.microsoft.com/java/whats-the-deal-with-humongous-objects-in-java/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://johngrib.github.io/wiki/java/gc/g1gc/#humongous-object&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://johngrib.github.io/wiki/java/gc/g1gc/#humongous-object&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/자바를 더 깊게</category>
      <author>Junuuu</author>
      <guid isPermaLink="true">https://junuuu.tistory.com/1042</guid>
      <comments>https://junuuu.tistory.com/1042#entry1042comment</comments>
      <pubDate>Sat, 15 Mar 2025 19:43:13 +0900</pubDate>
    </item>
    <item>
      <title>Long Transaction을 감지하고 대응하는 방법</title>
      <link>https://junuuu.tistory.com/1041</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Long Transaction으로 인해 DB에 부하가 발생하여 같은 DB를 활용하는 모든 서버들에 영향이 가는 상황이 발생하여 왜 그런 일이 일어나는지, 어떻게 하면 예방할 수 있을지 공부하며 기록해보고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Long Transaction이란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Long Transaction은 트랜잭션이 열리고 긴 시간 동안 commit, rollback이 수행되지 않고 있는 데이터베이스 트랜잭션을 말합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터베이스에서 트랜잭션은 하나의 논리적인 작업 단위를 의미하며, 데이터의 &lt;b&gt;일관성(Consistency)과 무결성(Integrity)&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;size18&quot;&gt;긴 시간이라는 것은 상대적이긴 개념입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 평균 트랜잭션이 100ms 이내에 처리된다면, 1초만 넘어가도 상대적으로 Long Transaction으로 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;반면, 데이터 마이그레이션과 같은 작업에서는 몇 분이 걸려도 Long Transaction으로 간주되지 않을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 서비스의 특성에 맞도록 임계치를 잡아 Long Transaction으로 분류해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Long Transaction 영향&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;첫 번째로 특정 rows, 특정 table에 트랜잭션이 lock을 잡고 놓아주지 않는다면 동일 rows, 동일 table에 추가적으로 lock을 획득하고자 하는 다른 스레드가 기다리게 되고 이 것이 심하면 전체장애로 번질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;두 번째로 MySQL 공용 내부 자원을 잠식하여 전체 지연으로 이어지는 케이스들도 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;InnoDB 엔진은 MVCC를 활용하여 트랜잭션 격리성을 보장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 Long Transaction이 &lt;b&gt;오랫동안 커밋되지 않으면&lt;/b&gt;, 변경된 데이터의 이전 버전을 &lt;b&gt;UNDO 로그&lt;/b&gt;에 저장해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이로 인해 UNDO 로그가 커지면서 &lt;b&gt;디스크 사용량이 급증&lt;/b&gt;하고, 성능 저하 발생 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;세 번째로 서버와 데이터베이스를 연동할 때 보통 Connection Pool을 활용하곤 하는데, 하나의 트랜잭션을 처리하는데 시간이 오래 걸리게 되면 요청이 증가함에 따라 Connection Pool이 고갈될 가능성이 큽니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Long Transaction 감지와 대응&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터베이스에 문제가 생기면 서비스를 할 수 없어지므로 Long Transaction을 감지할 수 있어야 하고, Long Transaction이 발생했다면 해당 Transaction을 Kill 하는 등 대응할 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;어떻게 Long Transaction인 경우 예외를 발생시켜서 Kill 할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Spring의 JPA를 활용중이라면 application.yml 설정으로 일괄적으로 세팅할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740215180812&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  transaction:
    default-timeout: 5 # 모든 트랜잭션의 기본 타임아웃 (초 단위)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;발생하는 예외&lt;/p&gt;
&lt;pre id=&quot;code_1740215217904&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.hibernate.TransactionException: transaction timeout expired
	at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.determineRemainingTransactionTimeOutPeriod(JdbcCoordinatorImpl.java:262) ~[hibernate-core-6.2.2.Final.jar:6.2.2.Final]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5초가 넘어가는 경우 transaction timeout expired라는 메시지와 함께 hibernate의 TransactionException이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 개별로 적용하고 싶다면 @Transactional 메서드에 값을 직접 주입하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740215403266&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Transactional(timeout = 10)
    fun makeLongTransaction() {
        repeat(5000){
            testEntityRepository.save(TestEntity())
            sleep(100)
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 실행중인 Transaction을 Kill 하는 것은 서비스에 어떤 사이드 이펙트를 가져올지 예측하기 어렵기 때문에 위험할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이때는 감지를 해서 임계치를 넘는 경우 제어해 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Hibernate Interceptor 기능을 활용해 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;트랜잭션이 시작되었을때, 시간을 저장해 두고 트랜잭션이 종료되었을 때 해당 시간이 지나간 경우 warn, error 로그등을 남겨두면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740220336357&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class TransactionTimeInterceptor : Interceptor {

    private val transactionStartTimes = ConcurrentHashMap&amp;lt;Transaction, Long&amp;gt;()

    override fun afterTransactionBegin(tx: Transaction) {
        transactionStartTimes[tx] = System.currentTimeMillis()
    }

    override fun afterTransactionCompletion(tx: Transaction) {
        val startTime = transactionStartTimes.remove(tx) ?: return
        val duration = System.currentTimeMillis() - startTime

        when {
            duration &amp;gt; 10_000 -&amp;gt; logger.warn(&quot;⚠️ [SLOW TRANSACTION] 실행 시간: {}ms (10초 초과!)&quot;, duration)
            duration &amp;gt; 5_000 -&amp;gt; logger.warn(&quot;⚠️ [SLOW TRANSACTION] 실행 시간: {}ms (5초 초과!)&quot;, duration)
            duration &amp;gt; 3_000 -&amp;gt; logger.info(&quot;✅ [NOTICE] 실행 시간: {}ms (3초 초과)&quot;, duration)
            else -&amp;gt; logger.debug(&quot;✅ [OK] 실행 시간: {}ms&quot;, duration)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ConcurrentHashMap에 Transaction을 저장해 두고, 끝날 때 시간을 측정하여 특정 시간이 넘어가는 경우 노티를 주게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://gokhansengun.com/why-do-long-db-transactions-affect-performance/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://gokhansengun.com/why-do-long-db-transactions-affect-performance/&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JPA</category>
      <author>Junuuu</author>
      <guid isPermaLink="true">https://junuuu.tistory.com/1041</guid>
      <comments>https://junuuu.tistory.com/1041#entry1041comment</comments>
      <pubDate>Tue, 25 Feb 2025 01:04:22 +0900</pubDate>
    </item>
    <item>
      <title>토스뱅크에서의 1년 회고</title>
      <link>https://junuuu.tistory.com/1040</link>
      <description>&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;2024년 1월 22일에 입사를 하고 어느덧 1년이 지나갔습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;미래의 나와 토스뱅크에 대해 궁금한 분들을 위해 1년의 여정을 기록으로 남겨보려고 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;입사 이유&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R5cbB/btsMcfXfGZf/itL9LHlOZ010oiqLLG9b8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R5cbB/btsMcfXfGZf/itL9LHlOZ010oiqLLG9b8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R5cbB/btsMcfXfGZf/itL9LHlOZ010oiqLLG9b8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR5cbB%2FbtsMcfXfGZf%2FitL9LHlOZ010oiqLLG9b8K%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;579&quot; height=&quot;393&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;금융 도메인이 저에게도, 많은 사람들에게도 불편한 영역이었습니다. 이 불편함을 해결하는 게 기여하고 싶었습니다. 포용과 혁신을 추구하는 토스뱅크에서 함께 일한다면 더 큰 시너지를 낼 수 있을 거라 생각했습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;또한 빠르게 성장하는 회사에 합류하여 도전적인 미션들을 해결하면서 회사와 같이 개인적으로도 성장하는 것을 기대하였습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;업무와 역할&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI4MUV/btsMbUZ9iV3/YA3PT5byENw9G9FOADwboK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI4MUV/btsMbUZ9iV3/YA3PT5byENw9G9FOADwboK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI4MUV/btsMbUZ9iV3/YA3PT5byENw9G9FOADwboK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI4MUV%2FbtsMbUZ9iV3%2FYA3PT5byENw9G9FOADwboK%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;630&quot; height=&quot;354&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;입사 후 합류하게 된 팀은 기업여신 팀이었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;기업여신팀은 개인사업자분들에게 대출을 제공하는 도메인이었으며 시장 상황에 따라 폐업이 발생할 수 있는 개인사업자분들에게 대출을 제공하는 것은 리스크가 크므로 보통 소상공인을 지원하는 대외기관(소상공인시장진흥공단, 신용보증기금, 신용보증재단)과 협력하여 기관에서 발급한 보증서를 기반으로 대출을 내어주게 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;이런 이유로 기업여신팀에서 Server Developer의 역할은 토스뱅크의 기술과 정책, 대외기관의 기술과 정책을 이어주는 어댑터 역할을 하는 서버를 구성하여 신규 상품을 개발하고 기존 상품을 유지보수하는 역할을 맡습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&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-origin-width=&quot;1280&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beDZBu/btsMbWcy6nC/vFDe7xLzgC5koKRv6jVYs0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beDZBu/btsMbWcy6nC/vFDe7xLzgC5koKRv6jVYs0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beDZBu/btsMbWcy6nC/vFDe7xLzgC5koKRv6jVYs0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeDZBu%2FbtsMbWcy6nC%2FvFDe7xLzgC5koKRv6jVYs0%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;499&quot; height=&quot;349&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;입사 후 온보딩 과제로 대외기관의 신규 상품을 추가하는 작업을 맡게 되었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;기존 직장에서 Kotlin, Spring, JPA, Kafka 등을 사용해 익숙했지만 같은 기술 스택이라도 환경이 바뀌었기 때문에 스타일이 달랐고 도메인의 요구사항이 달랐습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;대외기관과 연동하기 위해 REST API 형태의 JSON 대신 XML을 활용해 보거나 혹은 TCP 통신을 통해 전문통신이라 부르는 프로토콜이 활용되기도 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;외부 네트워크에 많이 의존을 해야 하기 때문에 외부 호출의 예외처리 혹은 네트워크 타임아웃에 대한 고려도 필요할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;또한 대외기관에게 파일을 전송하기 위해 SFTP 프로토콜을 학습하거나 테스트의 외부 의존성을 줄이기 위해 Redis를 기반으로 한 Feature Flag를 활용하여 테스트 생산성을 극대화하기도 했습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;Comfort Zone을 넘어선 경험을 했으며 모르는 영역이 너무 많아 두렵기도 했고 학습하는 과정이 때로는 고통스러웠지만 모르는 영역은 동료들에게 적극적으로 도움을 구했고 친절하게 알려주시는 분들 덕분에 빠르게 적응할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;신규 상품을 추가하는 과정에서 신청, 서류, 심사, 실행, 상환 등의 전반적인 영역에 대한 이해가 필요했고 이를 기반으로 프로세스를 구현해야 했기 때문에 빠르게 팀에 적응할 수 있었던 최고의 온보딩이었던 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;또한 개인적인 목표로 팀의 모든 코드리뷰에 참여하여 기술적, 정책적으로 궁금한 부분을 지속적으로 질문하고 학습해 나갔던 과정도 큰 도움이 되었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;최종적으로 인터넷뱅킹 최초로 신용보증기금의 이지원보증대출을 출시하였고 &lt;a href=&quot;https://www.sedaily.com/NewsView/2DCWCP33YL&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;각종 기사&lt;/a&gt;로도 소개되어 굉장히 뿌듯했습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;위기와 기회&lt;/b&gt;&lt;/h4&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;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brNpuP/btsMcqRLYYK/iISRBgk6T7fmY13zVd7vz1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brNpuP/btsMcqRLYYK/iISRBgk6T7fmY13zVd7vz1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brNpuP/btsMcqRLYYK/iISRBgk6T7fmY13zVd7vz1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrNpuP%2FbtsMcqRLYYK%2FiISRBgk6T7fmY13zVd7vz1%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;609&quot; height=&quot;343&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;토스뱅크에서는 팀 내의 이동이 자유롭습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;제가 어느 정도 적응해가고 있을 때 기존 구성원분들이 대부분 다른 스쿼드로 이동하게 되었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;자연스럽게 기존 구성원들의 역할을 위임받으며 새로운 기회를 경험했습니다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;이전에 상품을 출시했던 경험으로 새로운 상품을 출시할 때 신규입사자분들에게 업무 가이드를 제공하거나 기존에 시도해보지 않았던 영역들을 주도적으로 설계하고 도입해 볼 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;가이드 없이도 독립적으로 역할을 수행할 수 있게 되었고 의사결정할 때 다른 사람의 컨펌 없이 수행할 수 있는 자신감이 많이 생기게 되었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;또한 토스뱅크에서는 플랫폼팀에서 제품을 편하게 개발할 수 있도록 분산락, 로깅 시스템, kafka dql, batchJob 등을 다양한 기능들을 라이브러리화 하여 지원합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;장단점이 있다고 생각하는데 장점으로는 제품 개발자가 공통 기능에 대해 고민할 시간을 줄여주어 생산성을 향상하고 반대로 단점으로는 제품 개발자가 공통 기능에 대해 고찰해 볼 시간이 줄어들어 기술적인 깊이가 낮아져 이슈가 발생했을 때 대응하기 어려워질 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;고민을 하다가 사내 라이브러리를 단순 사용하는 것을 넘어 내부동작을 이해해 보며 더 나아가 개선점을 찾고 건의 및 반영하거나 문서화에 기여하는 길드를 만들었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;위 과정을 통해 추후 라이브러리를 활용하시는 분들에게 도움이 되고자 문서화에 기여하였습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;또한 라이브러리와 관련된 기능을 활용할 때는 자신감을 얻고 몇몇 기능들은 버그를 수정하거나 리마인드 메시지가 안내되도록 기여했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문화와 가치관&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;669&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1RwuI/btsMa4PZtRk/sjJZNBBzX9zLWkp9KnpMN1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1RwuI/btsMa4PZtRk/sjJZNBBzX9zLWkp9KnpMN1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1RwuI/btsMa4PZtRk/sjJZNBBzX9zLWkp9KnpMN1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1RwuI%2FbtsMa4PZtRk%2FsjJZNBBzX9zLWkp9KnpMN1%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;616&quot; height=&quot;343&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;669&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;토양어선, 스트라이크 제도 등 부정적인 인식들이 존재합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;입사 전에는 날카로운 피드백을 받진 않을까 잘 적응할 수 있을까에 대해서도 걱정했습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;하지만 스트라이크 제도는 오래전에 폐지되고 TRP라는 제도가 등장하였지만 주변에서 실제 사례는 접해보지 못했던 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;또한 개인적으로는 주 40시간보다는 일을 많이 하지만 정해진 법정근로시간 안에서 일하며 추가 근로 시간에 대한 보상도 적절하게 이루어집니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;근무시간의 경우 코어타임이 존재하지 않고 자율적으로 구성원이 조절할 수 있기 때문에 압도적인 자유가 주어집니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;원하는 시간에 쉬고, 원하는 시간에 일할 수 있어 감사함을 느낍니다. 이것이 진정한 워라밸이 아닐까 생각합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;추가 근로에 대한 선택도 개인에게 있기 때문에 자유 의지로 선택할 수 있으며 개인적으로는 아직은 일하는 게 즐거워서 기존 업무 이외에도 여러 방면에서 기여할 부분을 찾아서 지내고 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;입사하기 전에는 3~4년 정도 다니지 않을까 생각했는데 이런 문화가 지속되고 제 가치관이 바뀌지 않는다면 7~8년 혹은 그 이상도 다닐 수 있지 않을까?라는 생각도 들었습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;이외에도 내부 100 외부 0이라는 문화로 구성원들에게 모든 정보가 공개되어 의사결정을 더 잘할 수 있도록 지원하며 무제한 법인카드 등 자율과 책임이라는 문화가 말뿐만 아니라 피부로 느낄 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;앞으로의 목표&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcYMqM/btsMbEJTxz4/xkN6ndw9IjRDPO5cNXuzi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcYMqM/btsMbEJTxz4/xkN6ndw9IjRDPO5cNXuzi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcYMqM/btsMbEJTxz4/xkN6ndw9IjRDPO5cNXuzi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcYMqM%2FbtsMbEJTxz4%2FxkN6ndw9IjRDPO5cNXuzi1%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;536&quot; height=&quot;357&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;지난 1년은 새로운 환경에 적응하고 동료들의 신뢰를 쌓기 위해 노력했던 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;신뢰를 충분히 쌓았다고 느껴 앞으로의 1년은 다양한 영역에서 나의 1시간이 동료의 6시간을 아껴줄 수 있도록 기여해보고 싶습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;의식적으로 스쿼드에서 발생한 문제의 해결과정을 트라이브, 챕터에도 적용하여 동일한 문제를 푸는데 시간을 줄일 수 있도록 기여하고자 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;또한 비즈니스적인 임팩트를 만들어 보기 위해 OKR에 도달하기 위한 액션아이템을 정하고 이를 의식적으로 수행해보려고 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;현재 팀에서는 새로운 상품이 지속적으로 출시되고 이에 대한 운영 부담감도 높아질 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;팀 이동도 잦기 때문에 신규 인원이 합류했을 때 빠르게 적응할 수 있는 환경을 만드는 것이 목표입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;두 가지를 모두 잡기 위해서는 기술적&amp;middot;비즈니스적 복잡도를 줄여야 한다고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;다양한 상품별로 정책을 최대한 통일하여 인지부하를 줄여나가는 것이 목표입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;이외에도 좋은 문서화 방법들을 통하여 지속가능한 팀을 위한 고민들을 해보려고 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;토스뱅크는 채용 중&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;함께 금융을 혁신해보고 싶은 분들과 함께 일하고 싶습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;지원이 고민되거나 궁금한 점이 있다면 언제든지 커피챗 환영합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://toss.im/career/job-detail?job_id=4071141003&amp;amp;company=%ED%86%A0%EC%8A%A4%EB%B1%85%ED%81%AC&amp;amp;detailedPosition=Product&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Server Developer 채용공고&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <author>Junuuu</author>
      <guid isPermaLink="true">https://junuuu.tistory.com/1040</guid>
      <comments>https://junuuu.tistory.com/1040#entry1040comment</comments>
      <pubDate>Wed, 19 Feb 2025 20:21:16 +0900</pubDate>
    </item>
  </channel>
</rss>