<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코딩하는 오징어</title>
    <link>https://effectivesquid.tistory.com/</link>
    <description>코딩하는 오징어의 지식창고, 일상, IT정보등을 담고 있는 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 17 Apr 2026 03:54:47 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>코딩하는 오징어</managingEditor>
    <image>
      <title>코딩하는 오징어</title>
      <url>https://tistory1.daumcdn.net/tistory/2732191/attach/16ef3fd14a03439886a7a04edc207aba</url>
      <link>https://effectivesquid.tistory.com</link>
    </image>
    <item>
      <title>Reference Count를 통한 Netty의 ByteBuf memory 관리</title>
      <link>https://effectivesquid.tistory.com/entry/Reference-Count%EB%A5%BC-%ED%86%B5%ED%95%9C-Netty%EC%9D%98-ByteBuf-memory-%EA%B4%80%EB%A6%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Netty는 ByteBuf를 data structure로 이용하고 있으며 ByteBuf는 RefereneCount (ReferenceCounted interface를 상속하고 있다.)를 통해 memory를 release할 지 결정한다. 여기서 memory를 release를 한다는 건 어떤 의미일까? java는 garbage collector가 메모리를 알아서 관리해줄텐데 잘못된 reference count 관리에 의해서 memory leak이 발생한다는 건 어떤 의미일까? 그러기 위해서는 먼저 Java NIO ByteBuffer에서 생성된 buffer가 더 이상 참조되지 않을 때 어떤 방식으로 메모리가 수집 되는 지 살펴보아야한다. &lt;b&gt;&lt;span style=&quot;color: #eb5757;&quot; data-token-index=&quot;1&quot;&gt;ByteBuffer.allocate()&lt;/span&gt;&lt;/b&gt; 를 이용하여 heap buffer로 생성한다면 (참조 하는 변수를 null로 세팅할 경우) gc가 발생했을 때 알아서 수집되겠지만&lt;b&gt; &lt;span style=&quot;color: #eb5757;&quot; data-token-index=&quot;3&quot;&gt;ByteBuffer.allocateDirect()&lt;/span&gt; &lt;/b&gt;를 통해 kernel buffer를 생성한다면 (non-heap) 해당 buffer는 언제, 어떻게 메모리를 정리하게 되는지 살펴보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;ByteBuffer.allocateDirect(int)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1681493127039&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java nio의 ByteBuffer.allocateDirect method를 호출하게 되면 DirectByteBuffer 생성자를 통해 kernel buffer를 얻어온다. DirectByteBuffer 생성자는 어떻게 생겼는지 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kloln/btsaibq6fRI/2QpDsEUnO8xLAg4ZmhjDjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kloln/btsaibq6fRI/2QpDsEUnO8xLAg4ZmhjDjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kloln/btsaibq6fRI/2QpDsEUnO8xLAg4ZmhjDjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKloln%2Fbtsaibq6fRI%2F2QpDsEUnO8xLAg4ZmhjDjk%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;529&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DirectByteBuffer의 생성자는 위와 같이 작성 되어있으며 우리가 주의깊게 봐야할 부분은 세 가지이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UNSAFE&lt;/li&gt;
&lt;li&gt;Deallocator&lt;/li&gt;
&lt;li&gt;Cleaner&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;UNSAFE (jdk.internal.misc)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 객체는 Singleton으로 생성되며 JNI를 통해 memory를 시스템 콜을 호출하며 memory를 할당하거나 해제하는 method를 제공한다. kernel buffer를 생성할 때 해당 객체를 이용하여 memory를 할당하거나 해제한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;memory allocate: Unsafe.allocateMemory()&lt;/li&gt;
&lt;li&gt;memory free: Unsafe.freeMemory&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;Deallocator&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2455&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTx7RQ/btsajdhV9im/Y6YjWBd4Iq3QZ0zQvwmLuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTx7RQ/btsajdhV9im/Y6YjWBd4Iq3QZ0zQvwmLuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTx7RQ/btsajdhV9im/Y6YjWBd4Iq3QZ0zQvwmLuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTx7RQ%2FbtsajdhV9im%2FY6YjWBd4Iq3QZ0zQvwmLuK%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;459&quot; height=&quot;563&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2455&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DirectByteBuffer 내부 클래스이며 run method에서 kernel memory를 정리해주며 해당 method는 아래에 설명할 Cleaner method에서 호출해준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;Cleaner&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1062&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6hX8h/btsahJobNlf/kFy4DLgfmMO9SKp1iozoG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6hX8h/btsahJobNlf/kFy4DLgfmMO9SKp1iozoG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6hX8h/btsahJobNlf/kFy4DLgfmMO9SKp1iozoG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6hX8h%2FbtsahJobNlf%2FkFy4DLgfmMO9SKp1iozoG0%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;611&quot; height=&quot;324&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1062&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cleaner는 &lt;span data-token-index=&quot;1&quot;&gt;&lt;b&gt;PhantomReference&amp;lt;Object&amp;gt;&lt;/b&gt;&amp;nbsp; &lt;/span&gt;상속하며 &lt;b&gt;&lt;span data-token-index=&quot;3&quot;&gt;ReferenceQueue&lt;/span&gt;&lt;/b&gt;를 이용하여 &lt;b&gt;&lt;span data-token-index=&quot;5&quot;&gt;clean method&lt;/span&gt;&lt;/b&gt;를 호출한다. 이 부분이 direct buffer를 memory에서 정리해주는 key point이므로 기억해두자. 이후에 PhantomReference와 ReferenceQueue에 대해 살펴 볼 때 해당 부분이 왜 key point인지 이해할 수 있을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cW5YDu/btsaf7wkcQI/TdSjk98zbO4MhoFJV1Nbo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cW5YDu/btsaf7wkcQI/TdSjk98zbO4MhoFJV1Nbo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cW5YDu/btsaf7wkcQI/TdSjk98zbO4MhoFJV1Nbo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcW5YDu%2Fbtsaf7wkcQI%2FTdSjk98zbO4MhoFJV1Nbo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;559&quot; height=&quot;695&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DirectByteBuffer 생성자에서 member 변수로 갖고 있는 Cleaner를 초기화하는 부분을 다시 살펴보면 create method를 통해 초기화해주고 있다는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1681493432678&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DirectByteBuffer(int cap) {
    ...
    long base = 0;
    try {
        base = UNSAFE.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    UNSAFE.setMemory(base, size, (byte) 0);
    ...
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); // cleaner를 초기화
    att = null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clean의 thunk member 변수는 DirectByteBuffer의 내부 클래스인 Deallocator로 초기화되며 clean method가 호출되면 Deallocator의 run method를 호출하게 된다. Deallocator의 run method에서는 &lt;b&gt;Unsafe.freeMemory(int)&lt;/b&gt; 를 통해 kernel buffer를 정리해주는 것을 확인할 수 있다. 그럼 Cleaner 클래스의 clean method는 언제, 어떻게 호출될까? 이 부분만 명확해진다면 &lt;b&gt;ByteBuffer.allocateDirect(int)&lt;/b&gt; 를 통해 생성한 kernel buffer가 언제 , 어떻게 정리되는지 확인된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cleaner는 &lt;b&gt;PhantomReference&amp;lt;Object&amp;gt;&lt;/b&gt;&amp;nbsp; 상속하며 &lt;b&gt;ReferenceQueue&lt;/b&gt;를 이용하여 &lt;b&gt;clean method&lt;/b&gt;를 호출한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 코드를 살펴볼 때 잠깐 언급한 위의 내용을 떠올려보자. 이게 어떤 의미를 갖고 있는지 설명해보려고한다. java는 개발자가 gc에 직접적으로 관여할 수 없다. 하지만 언어에서 제공하는 Reference mechanism을 이용하면 간접적으로 gc에 관여할 수 있게된다. Java GC는 객체가 gc 대상인지 판별하기 위해서 reachability라는 개념을 사용한다. 어떤 객체에 유효한 참조가 있으면 'reachable'로, 없으면 'unreachable'로 구별하고, unreachable 객체를 gc 대상으로 인식한다. 한 객체는 여러 다른 객체를 참조하고, 참조된 다른 객체들도 마찬가지로 또 다른 객체들을 참조할 수 있으므로 객체들은 참조 사슬을 이룬다. 이런 상황에서 유효한 참조 여부를 파악하려면 항상 유효한 최초의 참조가 있어야 하는데 이를 객체 참조의 root set이라고 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1681493492098&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ReferenceExample {

    private Object dummpy = new Object();
    
    public void makeGcTargetForDummpy() {
        this.dummpy = null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 간단한 코드는 dummy 객체를 gc대상으로 만들기 위해 null로 세팅하는 코드다. null로 세팅함으로써 dummy 변수에 할당된 Object 객체를 더 이상 참조되지 않도록 하는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEZ6Bl/btsak30aF5Z/NQHbyv5RbkaNaxmx9LCk0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEZ6Bl/btsak30aF5Z/NQHbyv5RbkaNaxmx9LCk0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEZ6Bl/btsak30aF5Z/NQHbyv5RbkaNaxmx9LCk0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEZ6Bl%2Fbtsak30aF5Z%2FNQHbyv5RbkaNaxmx9LCk0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;537&quot; height=&quot;356&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 설명한 것처럼, 원래 GC 대상 여부는 reachable인가 unreachable인가로만 구분하였고 이를 사용자 코드에서는 관여할 수 없었다. 그러나 java.lang.ref 패키지를 이용하여 reachable 객체들을 strongly reachable, softly reachable, weakly reachable, phantomly reachable로 더 자세히 구별하여 GC 때의 동작을 다르게 지정할 수 있게 되었다. 다시 말해, GC 대상 여부를 판별하는 부분에 사용자 코드가 개입할 수 있게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/opkg4/btsakeU5ik1/GjmvheCch86KB8dxjULGlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/opkg4/btsakeU5ik1/GjmvheCch86KB8dxjULGlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/opkg4/btsakeU5ik1/GjmvheCch86KB8dxjULGlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fopkg4%2FbtsakeU5ik1%2FGjmvheCch86KB8dxjULGlk%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;538&quot; height=&quot;357&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 내용은 첨부한 글을 살펴보면 좋을 것 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java Reference와 GC: &lt;a href=&quot;https://d2.naver.com/helloworld/329631&quot;&gt;https://d2.naver.com/helloworld/329631&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 위의 네 가지 reachable 중에서 phantomly reachable에 대해 이해해야한다. phantomly reachable을 살펴보기 전에 먼저 ReferenceQueue에 대해 살펴보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;ReferenceQueue&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SoftReference 객체나 WeakReference 객체가 참조하는 객체가 GC 대상이 되면 SoftReference 객체, WeakReference 객체 내의 참조는 null로 설정되고 SoftReference 객체, WeakReference 객체 자체는 ReferenceQueue에 enqueue된다. ReferenceQueue에 enqueue하는 작업은 GC에 의해 자동으로 수행된다. ReferenceQueue의 poll() 메서드나 remove() 메서드를 이용해 ReferenceQueue에 이들 reference object가 enqueue되었는지 확인하면 softly reachable 객체나 weakly reachable 객체가 GC되었는지를 파악할 수 있고, 이에 따라 관련된 리소스나 객체에 대한 후처리 작업을 할 수 있다. 어떤 객체가 더 이상 필요 없게 되었을 때 관련된 후처리를 해야 하는 애플리케이션에서 이 ReferenceQueue를 유용하게 사용할 수 있다. Java Collections 클래스 중에서 간단한 캐시를 구현하는 용도로 자주 사용되는 WeakHashMap 클래스는 이 ReferenceQueue와 WeakReference를 사용하여 구현되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SoftReference와 WeakReference는 ReferenceQueue를 사용할 수도 있고 사용하지 않을 수도 있다. 이는 이들 클래스의 생성자 중에서 ReferenceQueue를 인자로 받는 생성자를 사용하느냐 아니냐로 결정한다. 그러나 &lt;b&gt;PhantomReference는 반드시 ReferenceQueue를 사용해야만 한다. PhantomReference의 생성자는 단 하나이며 항상 ReferenceQueue를 인자로 받는다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1681493660744&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public PhantomReference(T referent, ReferenceQueue&amp;lt;? super T&amp;gt; q) {
    super(referent, q);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;Phantomly Reachable과 PhantomReference&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC 대상 객체를 찾는 작업과 GC 대상 객체를 처리하는 작업이 연속적이지 않 듯이, GC 대상 객체를 처리하는 작업과 할당된 메모리를 회수하는 작업도 연속된 작업이 아니다. GC 대상 객체를 처리하는 작업, 즉 객체의 파이널라이즈 작업이 이루어진 후에 GC 알고리즘에 따라 할당된 메모리를 회수한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC 대상 여부를 결정하는 부분에 관여하는 softly reachable, weakly reachable과는 달리, phantomly reachable은 파이널라이즈와 메모리 회수 사이에 관여한다. strongly reachable, softly reachable, weakly reachable에 해당하지 않고 PhantomReference로만 참조되는 객체는 먼저 파이널라이즈된 이후에 phantomly reachable로 간주된다. 다시 말해, 객체에 대한 참조가 PhantomReference만 남게 되면 해당 객체는 바로 파이널라이즈된다. GC가 객체를 처리하는 순서는 항상 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;soft references&lt;/li&gt;
&lt;li&gt;weak references&lt;/li&gt;
&lt;li&gt;파이널라이즈&lt;/li&gt;
&lt;li&gt;phantom references&lt;/li&gt;
&lt;li&gt;메모리 회수&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 어떤 객체에 대해 GC 여부를 판별하는 작업은 이 객체의 reachability를 strongly, softly, weakly 순서로 먼저 판별하고, 모두 아니면 phantomly reachable 여부를 판별하기 전에 파이널라이즈를 진행한다. 그리고 대상 객체를 참조하는 PhantomReference가 있다면 phantomly reachable로 간주하여 PhantomReference를 ReferenceQueue에 넣고 파이널라이즈 이후 작업을 애플리케이션이 수행하게 하고 메모리 회수는 지연시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명한 것처럼 PhatomReference는 항상 ReferenceQueue를 필요로 한다. 그리고 PhantomReference의 get() 메서드는 SoftReference, WeakReference와 달리 항상 null을 반환한다. 따라서 한 번 phantomly reachable로 판명된 객체는 더 이상 사용될 수 없게 된다. 그리고 &lt;b&gt;phantomly reachable로 판명된 객체에 대한 참조를 GC가 자동으로 null로 설정하지 않으므로, 후처리 작업 후에 사용자 코드에서 명시적으로 clear() 메서드를 실행하여 null로 설정해야 메모리 회수가 진행된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같이, PhantomReference를 사용하면 어떤 객체가 파이널라이즈된 이후에 할당된 메모리가 회수되는 시점에 사용자 코드가 관여할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Phantom Reference example&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/java-phantom-reference&quot;&gt;https://www.baeldung.com/java-phantom-reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://luckydavekim.github.io/development/back-end/java/phantom-reference-in-java&quot;&gt;https://luckydavekim.github.io/development/back-end/java/phantom-reference-in-java&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;Release Kernel Memory &lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;Allocated&lt;/span&gt;&lt;/b&gt; By DirectByteBuffer&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Phantom Reference에 대해 살펴보았으니 다시 본래의 목적인 DirectByteBuffer에서 할당한 kernel memory를 언제 release해주는지 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l7qOI/btsak3FR5Vy/t2EgfQD2T3nAriYN7siwKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l7qOI/btsak3FR5Vy/t2EgfQD2T3nAriYN7siwKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l7qOI/btsak3FR5Vy/t2EgfQD2T3nAriYN7siwKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl7qOI%2Fbtsak3FR5Vy%2Ft2EgfQD2T3nAriYN7siwKk%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;523&quot; height=&quot;553&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DirectByteBuffer의 생성자를 다시 살펴보면 cleaner 변수를 초기화할 때 Deallocator를 생성하여 Cleaner의 thunk 변수로 초기화한다. Deallocator와 Cleaner 코드를 다시 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에 주석을 달아두었다. 해당 주석을 통해 clean method가 언제 호출되는지 이해할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;Deallocator&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1681493855798&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static class Deallocator implements Runnable {

    private long address;
    private long size;
    private int capacity;

    private Deallocator(long address, long size, int capacity) {
        assert (address != 0);
        this.address = address;
        this.size = size;
        this.capacity = capacity;
    }

    // Cleaner의 clean method에서 thunk.run()을 통해 호출된다.
    public void run() {
        if (address == 0) {
            return;
        }
        UNSAFE.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;Cleaner&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1681493952134&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Cleaner extends PhantomReference&amp;lt;Object&amp;gt; {

    private static final ReferenceQueue&amp;lt;Object&amp;gt; dummyQueue = new ReferenceQueue&amp;lt;&amp;gt;();

    ...

    private final Runnable thunk;

    private Cleaner(Object referent, Runnable thunk) {
        super(referent, dummyQueue);
        this.thunk = thunk;
    }

    // DirectByteBuffer 생성자에서 Cleaner.create(this, new Deallocator(base, size, cap))을 호출한다.
    // 즉, PhantomReference의 referent로 들어가는 객체는 DirectByteBuffer이다.
    // DirectByteBuffer 객체가 PhantomReference로만 참조되는 순간에 ReferenceQueue에 PhantomRefernce 객체가 enqueue된다.
    // 이때, PhantomRefernce 객체는 Cleaner이며 ReferenceQueue에서 poll하여 후처리를 실행하는 daemon thread에서 clean method를 호출하는 것이다.
    public static Cleaner create(Object ob, Runnable thunk) {
        if (thunk == null)
            return null;
        return add(new Cleaner(ob, thunk));
    }

    /**
     * Runs this cleaner, if it has not been run before.
     */
    public void clean() {
        if (!remove(this))
            return;
        try {
            // thunk는 Deallocator의 객체이므로 thunk.run 호출시 Deallocator의 run method가 호출된다.
            // Deallocator의 run method에서는 native method인 UNSAFE.freeMemory(long)을 호출하여 kernel buffer를 release한다.
            thunk.run();
        } catch (final Throwable x) {
            AccessController.doPrivileged(new PrivilegedAction&amp;lt;&amp;gt;() {
                    public Void run() {
                        if (System.err != null)
                            new Error(&quot;Cleaner terminated abnormally&quot;, x)
                                .printStackTrace();
                        System.exit(1);
                        return null;
                    }});
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;DirectByteBuffer의 생성자 &amp;amp; Deallocator &amp;amp; Cleaner&lt;/span&gt;&lt;/b&gt; 코드와 주석을 보면 다음과 같은 경우에 kernel buffer가 release 된다는 것을 이해할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1681493995306&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void directByteBufferGcTest() throws InterruptedException {
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    
    // PhantomReference(Cleaner)로만 참조된다.
    buffer = null; 

    // PhantomReference 객체가 참조하는 객체가 GC 대상이 되면 PhantomReference 객체 자체는 ReferenceQueue에 enqueue된다.
    // PhantomReference 객체는 Cleaner이며 ReferenceQueue에서 poll하여 후처리를 실행하는 daemon thread에서 clean method를 호출하게된다.
    // DirectByteBuffer객체가 gc됨과 동시에 UNSAFE.freeMemory(long)가 호출되어 kernel buffer도 정리하게된다.
    System.gc();
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;Reference Count를 통한 ByteBuf 관리&lt;/b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netty의 Reference Count를 통해 ByteBuf가 관리되는 방법은 지금까지 설명한 내용들을 모두 이해하였다면 어려울 것이 없다. netty의 ChannelHandler에서 관리해주는 &lt;b&gt;PooledByteBuf&lt;/b&gt;를 확인해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1681494047135&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;abstract class PooledByteBuf&amp;lt;T&amp;gt; extends AbstractReferenceCountedByteBuf {
    
    private final Handle&amp;lt;PooledByteBuf&amp;lt;T&amp;gt;&amp;gt; recyclerHandle;

    protected PoolChunk&amp;lt;T&amp;gt; chunk;
    protected long handle;
    // ByteBuffer가 저장된다.
    protected T memory;
    protected int offset;
    protected int length;

    ...
    
    @Override
    protected final void deallocate() {
        if (handle &amp;gt;= 0) {
            final long handle = this.handle;
            this.handle = -1;
            memory = null;
            chunk.decrementPinnedMemory(maxLength);
            chunk.arena.free(chunk, tmpNioBuf, handle, maxLength, cache);
            tmpNioBuf = null;
            chunk = null;
            cache = null;
            recycle();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ByteBuf의 deallocate() method를 살펴보면 &lt;b&gt;&lt;span style=&quot;color: #eb5757;&quot; data-token-index=&quot;1&quot;&gt;memory = null&lt;/span&gt;&lt;/b&gt; 를 통해 ByteBuffer의 참조를 제거한다. 그리고 &lt;span data-token-index=&quot;3&quot;&gt;deallocate&lt;/span&gt;는 PooledByteBuf 클래스의 부모 클래스인 &lt;span data-token-index=&quot;5&quot;&gt;AbstractReferenceCountedByteBuf&lt;/span&gt;에서 release() method를 호출 했을 때 특정 조건을 만족하면 호출된다. 여기서 특정 조건이라 함은 reference count가 0이 되었을 경우이다.&lt;/p&gt;
&lt;pre id=&quot;code_1681494107837&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
    ...
    
    @Override
    public boolean release() {
        return handleRelease(updater.release(this));
    }

    @Override
    public boolean release(int decrement) {
        return handleRelease(updater.release(this, decrement));
    }

    private boolean handleRelease(boolean result) {
        if (result) {
            deallocate();
        }
        return result;
    }

    /**
     * Called once {@link #refCnt()} is equals 0.
     */
    protected abstract void deallocate(); // PooledByteBuf에서 구현하고 있다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ByteBuf를 사용 후 release 해주지 않으면 refCnt는 0이 되지 않을 것이고 deallocate()는 호출되지 않을 것이다. 그렇게되면 PooledByteBuf는 사용중인 객체로 인식될 것 이고 object leak이 발생하게된다. &lt;b&gt;-Dio.netty.leakDetection.level=ADVANCED&lt;/b&gt; 를 활성화하여 Leak 로그를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 PooledByteBuf를 테스트한 코드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1681494176217&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void pooledByteBufTest() {
    // Setting pool size 1
    PooledByteBufAllocator allocator = new PooledByteBufAllocator(true, 1, 1, 4096, 0, 0, 0, true);

    // Allocate a ByteBuf instance
    ByteBuf buf1 = allocator.directBuffer(4096);
    Assertions.assertNotNull(buf1);

    // Release buf1 to make the memory available for allocation again
    buf1.release();

    // Attempt to allocate another ByteBuf instance
    ByteBuf buf2 = allocator.directBuffer(4096);
    // buf1.release()하였므로 buf1을 재사용한다.
    Assertions.assertSame(buf1, buf2); 

    // buf2.release()를 하지않았으므로 새로운 ByteBuf를 생성한다.
    ByteBuf buf3 = allocator.directBuffer(4096);
    Assertions.assertNotNull(buf3);
    Assertions.assertNotSame(buf1, buf3);

    ByteBuf buf4 = allocator.directBuffer(4096);
    ByteBuf buf5 = allocator.directBuffer(4096);
    ByteBuf buf6 = allocator.directBuffer(4096);
    // buf4.release();
    // buf5.release();
    // buf6.release();
    buf4 = null;
    buf5 = null;
    buf6 = null;
    while (true) {
        System.gc();
        Thread.sleep(500);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k9mUZ/btsaiqoik51/znM0RWCgVj9uvKFtxnQWEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k9mUZ/btsaiqoik51/znM0RWCgVj9uvKFtxnQWEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k9mUZ/btsaiqoik51/znM0RWCgVj9uvKFtxnQWEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk9mUZ%2Fbtsaiqoik51%2FznM0RWCgVj9uvKFtxnQWEK%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;2000&quot; height=&quot;1454&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드 작성 후 Deallocator의 run method가 실행되는지 여부를 체크하기 위해 해당 line이 실행되면 &lt;b&gt;String.format(&quot;Deallocator run invoked: %d&quot;, address)&lt;/b&gt; 를 출력하도록 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;i&gt;&lt;b&gt;case1. release를 호출하지 않았을 때&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;709&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tM4sN/btsaiasbcyW/JNz8z45OoqAylGFaqhK3uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tM4sN/btsaiasbcyW/JNz8z45OoqAylGFaqhK3uK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tM4sN/btsaiasbcyW/JNz8z45OoqAylGFaqhK3uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtM4sN%2FbtsaiasbcyW%2FJNz8z45OoqAylGFaqhK3uK%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;505&quot; height=&quot;179&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;709&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;i&gt;case2. release를 호출했을 때&lt;/i&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;2000&quot; data-origin-height=&quot;1324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqJpQn/btsah9z2pJ1/Zrv333IMVSy0yZJAKFkYT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqJpQn/btsah9z2pJ1/Zrv333IMVSy0yZJAKFkYT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqJpQn/btsah9z2pJ1/Zrv333IMVSy0yZJAKFkYT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqJpQn%2Fbtsah9z2pJ1%2FZrv333IMVSy0yZJAKFkYT1%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;503&quot; height=&quot;333&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PS:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Leak로그는 ByteBufAllocator를 통해서 ByteBuf를 생성해야 tracking되며,&amp;nbsp; new UnpooledDirectByteBuf(new UnpooledByteBufAllocator(true), 1024, 1024); 이렇게 직접 생성하면 Leak로그가 출력되지 않는다. toLeakAwareBuffer(buf); method는 ByteBufAllocator에서 호출해주고 있기 때문이다.&lt;/li&gt;
&lt;li&gt;다음과 같은 코드는 leak 로그가 찍히지만 실제로는 Deallocator.run method 가 호출된다. 참조값이 null이 되어 gc대상이 되기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1681494362954&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void byteBufMemoryLeakTest() throws InterruptedException {
    ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);
    for (int i = 0; i &amp;lt; 2000; i++) {
        ByteBuf buffer = Unpooled.directBuffer();
        buffer = null;

        System.gc();
    }

    while (true) {

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ 하지만 다음과 같은 코드는 참조 값이 null이 아니므로 memory leak이 발생할 수 있다. Connection을 사용 후 Connection Pool로 돌려주지 않으면 발생하는 문제와 동일하다.&lt;/p&gt;
&lt;pre id=&quot;code_1681494401810&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void byteBufMemoryLeakTest() throws InterruptedException {
    ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);        

    List&amp;lt;ByteBuf&amp;gt; bufferList = new ArrayList&amp;lt;&amp;gt;();
    for (int i = 0; i &amp;lt; 2000; i++) {
        bufferList.add(Unpooled.directBuffer(1999));
    }

    for (int i = 0; i &amp;lt; 2000; i++) {
        ByteBuf buffer = bufferList.get(i);
        buffer.writeBytes(&quot;hello bytebuf&quot;.getBytes(StandardCharsets.UTF_8));
        
        System.gc();
    }

    while (true) {

    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Framework/Netty</category>
      <author>코딩하는 오징어</author>
      <guid isPermaLink="true">https://effectivesquid.tistory.com/82</guid>
      <comments>https://effectivesquid.tistory.com/entry/Reference-Count%EB%A5%BC-%ED%86%B5%ED%95%9C-Netty%EC%9D%98-ByteBuf-memory-%EA%B4%80%EB%A6%AC#entry82comment</comments>
      <pubDate>Sat, 15 Apr 2023 02:48:22 +0900</pubDate>
    </item>
    <item>
      <title>WebFlux 사용시 WebClient를 써야하는 이유</title>
      <link>https://effectivesquid.tistory.com/entry/WebFlux-%EC%82%AC%EC%9A%A9%EC%8B%9C-WebClient%EB%A5%BC-%EC%8D%A8%EC%95%BC%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Asynchronous - Non Blocking framework가 화두가 된지 꽤 오랜 시간이 흘렀다. WebFlux, Akka, Node.js, Vert.X 같은 다양한 Asynchronous - Non Blocking 모델을 제공하는 Web Framework가 등장하였고, Spring 을 사용하는 다수의 팀들은 WebFlux를 선택하여 Blocking 모델인 WebMvc로 부터 벗어나 Asynchronous - Non Blocking을 지향하고 있다고 이야기하고 있다. 하지만 우리는 정말 Thread를 block 시키지 않도록 코드를 작성하고 있을까?? Asynchronous와 Non Blocking에 대한 이야기 부터 간단하게 소개하고 현재 우리는 어떤 방식으로 Asynchronous - Non Blocking 방식의 고성능 Server Application을 개발하고 있는지 고찰해보려고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Synchronous vs Asynchronous &amp;amp; Blocking vs Non Blocking&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Synchronous vs Asynchronous &amp;amp; Blocking vs Non Blocking에대한 내용은 관점에 따라 조금씩 차이가 있는 설명들이 있지만 필자는 Thread 관점에서 살펴보려고한다. (메서드를 호출하는 호출자 관점으로 이해하여도 무방하다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-09 오후 5.54.08.png&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTO3GB/btrLPouyTcl/7vdFfedCytYZZIWPJz6r30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTO3GB/btrLPouyTcl/7vdFfedCytYZZIWPJz6r30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTO3GB/btrLPouyTcl/7vdFfedCytYZZIWPJz6r30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTO3GB%2FbtrLPouyTcl%2F7vdFfedCytYZZIWPJz6r30%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;425&quot; height=&quot;325&quot; data-filename=&quot;스크린샷 2022-09-09 오후 5.54.08.png&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread-A가 어떤 task를 Thread-B에게 전달 후 task의 결과를 기다리는 상황이다. &lt;b&gt;task를 전달 후 바로 return 받지 못하고 있다면&lt;/b&gt; blocking 모델이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return을 받은 후에는 Thread-B가 task를 수행한 결과를 이용하여 Thread-A가 후 처리를 하고 있다. task의 결과를 &lt;b&gt;Thread-A(전달자)가 기다린 후 처리한다면&lt;/b&gt; synchronous 모델이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떠한 task를 다른 thread에게 전달 후 task가 끝날 때 까지 return을 받지 못하고 기다린다면 blocking 모델, task의 결과를 돌려받아 자기 자신이 처리한다면 synchronous 모델이라고 할 수 있다. 즉 위의 모델은 synchronous &amp;amp; blocking 모델이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-09 오후 6.01.08.png&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJKKeP/btrLOlY9463/0b1k8ZkNM8uiCdwJWH43cK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJKKeP/btrLOlY9463/0b1k8ZkNM8uiCdwJWH43cK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJKKeP/btrLOlY9463/0b1k8ZkNM8uiCdwJWH43cK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJKKeP%2FbtrLOlY9463%2F0b1k8ZkNM8uiCdwJWH43cK%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;425&quot; height=&quot;310&quot; data-filename=&quot;스크린샷 2022-09-09 오후 6.01.08.png&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread-A가 어떤 task를 Thread-B에게 전달 후 바로 응답을 받는 상황이다. 응답을 받은 후 Thread-A는 Thread-B에게 전달한 task의 결과를 더 이상 신경쓰지 않고 다른 일을 이어나간다. &lt;b&gt;task를 전달 후 바로 return을 받아 다른 일을 처리하고 있다면&lt;/b&gt; non blocking 모델이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;task의 결과에 따른 후처리는 Thread-B가 처리한다. 이때, task의 결과에 따라 어떤 후처리를 진행할 건지 Thread-A가 Thread-B에게 callback method 형태로 task와 함께 전달하기도 한다. Thread-A 입장에서는 task를 전달 후 해당 결과에 대한 후처리를 전혀 신경쓰지 않는다. task 결과에 대한 후처리는 Thread-B가 책임을 지고 있는 상황이다. 이러한 상황은 &lt;b&gt;Thread-A 시점&lt;/b&gt;에서는 asynchrounous 모델이다. 즉, 위의 모델은 asynchronous &amp;amp; non blocking 모델이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 내용들을 간단하게 다음과 같이 정리할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;blocking vs non blocking: task 전달 후 바로 return을 받을 수 있는지의 여부&lt;/li&gt;
&lt;li&gt;synchronous vs asynchronous: task의 결과에 대한 후 처리를 task를 전달한 thread가 처리하는지 task를 받은 thread가 처리하는지의 여부 (&lt;b&gt;task를 전달한 thread의 시점&lt;/b&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 개념을 잘 이해하였다면 synchronous &amp;amp; non blocking, asynchronous &amp;amp; blocking 모델도 생각해볼수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-09 오후 6.41.24.png&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BoyV7/btrLLXdzn3E/KIdH2nzd3b6VrUp26EOCCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BoyV7/btrLLXdzn3E/KIdH2nzd3b6VrUp26EOCCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BoyV7/btrLLXdzn3E/KIdH2nzd3b6VrUp26EOCCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBoyV7%2FbtrLLXdzn3E%2FKIdH2nzd3b6VrUp26EOCCK%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;425&quot; height=&quot;311&quot; data-filename=&quot;스크린샷 2022-09-09 오후 6.41.24.png&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 그림에서는 Thread-A가 task를 Thread-B에게 전달 후 바로 return 받아서 다른 일을 처리하지만, 다른 일을 처리 후 task의 결과를 기다리고 있는 모습을 볼 수 있다. Thread-A (task 전달자)가 결과에 대한 후처리를 진행하고 있기 때문이다. 즉, 위의 모델은 synchronous &amp;amp; non blocking 모델이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-09 오후 6.49.52.png&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyBgMV/btrLR8rgXJO/446o8c1BXKXr7dTEdK7Ts1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyBgMV/btrLR8rgXJO/446o8c1BXKXr7dTEdK7Ts1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyBgMV/btrLR8rgXJO/446o8c1BXKXr7dTEdK7Ts1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyBgMV%2FbtrLR8rgXJO%2F446o8c1BXKXr7dTEdK7Ts1%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;425&quot; height=&quot;310&quot; data-filename=&quot;스크린샷 2022-09-09 오후 6.49.52.png&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 위와 같이 asynchronous &amp;amp; blocking 모델도 그려 볼 수 있다. 위의 내용들을 정리한 그림은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-12 오후 7.30.07.png&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;753&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wczs0/btrLWmilvZC/FkNkUkvBEky3VOkfBgw5E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wczs0/btrLWmilvZC/FkNkUkvBEky3VOkfBgw5E0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wczs0/btrLWmilvZC/FkNkUkvBEky3VOkfBgw5E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwczs0%2FbtrLWmilvZC%2FFkNkUkvBEky3VOkfBgw5E0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;492&quot; data-filename=&quot;스크린샷 2022-09-12 오후 7.30.07.png&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;753&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;WebFlux &amp;amp; WebClient&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring 5.x 부터 webflux와 webclient가 등장하였고 asynchronous &amp;amp; non blocking 모델로 고성능의 web server가 필요할 때 선택할 수 있는 도구이다. webflux를 사용한 사례를 작성한 글이 많이 나오기 시작하였고, 여러 컨퍼런스에서도 webflux를 사용하면 asynchronous &amp;amp; non blocking 메커니즘으로 인해 servlet 기반의 request per thread 모델보다 더 나은 성능지표가 나온다고 소개하기 시작했다. webflux의 내장 http 서버로는 여러 선택지가 있지만 default는 reactor netty를 사용한다. reactor netty는 netty를 reactor로 wrapping한 framework이며, netty는 java nio를 더욱 손쉽게 사용할 수 있도록 wrapping한 network programming framework이다. 그렇다면 정말 webflux만 사용하면 asynchronous &amp;amp; non blocking 모델을 통해 모든 thread들이 block되지 않고 cpu를 잘 활용할 수 있게 되는 걸까? 그렇지 않다. webflux만으로는 요청을 처리하는 모든 flow를 asynchronous &amp;amp; non blocking하게 할 수 없다. 아직까지 jdbc는 blocking 모델이고, asynchronous &amp;amp; non blocking을 지원하는 network I/O client를 사용하지 않는다면 thread는 blocking되게 된다. 이를 해결하기 위해 대부분 webflux에서 사용하는 event loop worker thread외에 별도의 thread pool을 만들어 해당 thread pool에 blocking task를 전달함으로써 event loop worker thread만 blocking을 피하도록 한다. 결국 어떤지점에서는 thread가 block되는 것이다. kotlin coroutine은 복잡한 asynchronous 프로그래밍을 syntax sugar를 통해 imperative하게 짤 수 있도록 도와주는 도구이다. 이 또한 coroutine context thread pool의 thread들은 blocking이된다. 이런 blocking이 잘못된 것이 아니다. non blocking을 지원하지 않는 라이브러리를 써야만하는 상황은 굉장히 일반적이고, non blocking 흐름을 유지하려면 앞서 설명한 별도의 thread pool에 던져놓는 방식이 아니라면 마땅한 해결책이 없다. 다만, 정말 모든 thread를 blocking 하지 않는 흐름은 없는 걸까? (이 글에서는 설명하지 않지만 proactor pattern도 참고해보자.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Java NIO&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java io는 Stream으로 data를 read &amp;amp; write 하며 InputStream과 OutStream을 별도로 구성하여 read &amp;amp; write를 처리한다. 즉, 단반향이다. Stream을 read &amp;amp; write 할 때는 작업이 끝날 때까지 thread가 block된다. 이에 따라 여러 요청을 처리하려면 여러 thread가 필요하다. 하지만 java nio는 하나의 Channel에서 read &amp;amp; write 하며 (양방향) Selector를 통해 read &amp;amp; write가 끝났을 때 이벤트를 받아 작업을 이어나간다. 즉, thread를 block 시키지 않기 때문에 하나의 thread로 여러 요청을 처리할 수 있다. 단, java nio를 사용한다고 모든 io가 non blocking으로 처리되지는 않는다. non blocking을 지원하는 것이지 non blocking 모드로 설정 하지 않으면 blocking으로 동작한다. socket channel은 non blocking모드가 가능하지만 file channel은 non blocking 모드를 지원하지 않는다. java7에서 AsynchronousFileChannel을 제공하지만 select() 메커니즘과는 다른 방법으로 non blocking을 지원한다고 한다. (&amp;rArr; 내부 thread pool로 작업을 위임한 후 바로 return 하는 방식)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;549&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ce1Wnx/btrLOno9bGz/6wjLFy8AJxNz19ZkT9VoDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ce1Wnx/btrLOno9bGz/6wjLFy8AJxNz19ZkT9VoDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ce1Wnx/btrLOno9bGz/6wjLFy8AJxNz19ZkT9VoDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fce1Wnx%2FbtrLOno9bGz%2F6wjLFy8AJxNz19ZkT9VoDk%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;455&quot; height=&quot;549&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;549&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 더해 기존의 java io는 커널 버퍼를 직접 읽어올 방법이 없어 jvm의 내부로 복사 한후 사용한다. 커널 버퍼의 값을 jvm 내부로 복사할 때 thread들은 block 상태가 된다. java nio는 커널 버퍼를 직접 read &amp;amp; write 할 수 있는 ByteBuffer를 제공하기 때문에 kernel buffer에서 jvm heap으로 한 번더 copy를 하는 overhead가 없다. 이를 zero copy라고 하는데 해당 내용은 이번 글에서 다루지 않겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;233&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boVp1l/btrLQmXA3r7/Ut4Y2X6LgqBSZG3wbd9Cyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boVp1l/btrLQmXA3r7/Ut4Y2X6LgqBSZG3wbd9Cyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boVp1l/btrLQmXA3r7/Ut4Y2X6LgqBSZG3wbd9Cyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboVp1l%2FbtrLQmXA3r7%2FUt4Y2X6LgqBSZG3wbd9Cyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;233&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;233&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Netty&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netty는 저수준의 nio를 wrapping한 network I/O framework이다. Netty관련 자세한 내용은 다른 글에서 다루고 있으니 해당 글을 참고하면 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://effectivesquid.tistory.com/entry/Netty의-스레드-모델&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2020.06.14 - [Framework/Netty] - Netty의 스레드 모델&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662962363909&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;Netty의 스레드 모델&quot; data-og-description=&quot;Netty는 비동기 네트워크 프레임워크 입니다. 이번 글에서는 이 비동기 프레임워크가 어떻게 동작하는지 살펴 보겠습니다. Netty는 Channel에서 발생하는 이벤트들을 EventLoop가 처리하는 구조입니다.&quot; data-og-host=&quot;effectivesquid.tistory.com&quot; data-og-source-url=&quot;https://effectivesquid.tistory.com/entry/Netty의-스레드-모델&quot; data-og-url=&quot;https://effectivesquid.tistory.com/entry/Netty%EC%9D%98-%EC%8A%A4%EB%A0%88%EB%93%9C-%EB%AA%A8%EB%8D%B8&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bw0AyB/hyPLovEFod/LgAYdAenUvuMsu67SLJd2K/img.png?width=703&amp;amp;height=282&amp;amp;face=0_0_703_282,https://scrap.kakaocdn.net/dn/NroPv/hyPLqAerto/RYzRf7moBQxUUXybF28YUk/img.png?width=703&amp;amp;height=282&amp;amp;face=0_0_703_282,https://scrap.kakaocdn.net/dn/bYDYmd/hyPLnjcmV7/2adWclkYxq3u4maqrIlkI0/img.png?width=700&amp;amp;height=557&amp;amp;face=0_0_700_557&quot;&gt;&lt;a href=&quot;https://effectivesquid.tistory.com/entry/Netty의-스레드-모델&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://effectivesquid.tistory.com/entry/Netty의-스레드-모델&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bw0AyB/hyPLovEFod/LgAYdAenUvuMsu67SLJd2K/img.png?width=703&amp;amp;height=282&amp;amp;face=0_0_703_282,https://scrap.kakaocdn.net/dn/NroPv/hyPLqAerto/RYzRf7moBQxUUXybF28YUk/img.png?width=703&amp;amp;height=282&amp;amp;face=0_0_703_282,https://scrap.kakaocdn.net/dn/bYDYmd/hyPLnjcmV7/2adWclkYxq3u4maqrIlkI0/img.png?width=700&amp;amp;height=557&amp;amp;face=0_0_700_557');&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;Netty의 스레드 모델&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Netty는 비동기 네트워크 프레임워크 입니다. 이번 글에서는 이 비동기 프레임워크가 어떻게 동작하는지 살펴 보겠습니다. Netty는 Channel에서 발생하는 이벤트들을 EventLoop가 처리하는 구조입니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;effectivesquid.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://effectivesquid.tistory.com/entry/Netty의-기본-Component-및-Architecture&quot;&gt;2020.05.17 - [Framework/Netty] - Netty의 기본 Component 및 Architecture&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1662962383236&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;Netty의 기본 Component 및 Architecture&quot; data-og-description=&quot;Netty는 네트워크적인 low한 처리와 비즈니스 로직 처리를 추상화를 통해 분리하였으며 덕분에 비즈니스 로직에 더욱 집중할 수 있도록 도와준다. Netty는 크게 다음과 같은 Component들을 통해 데이&quot; data-og-host=&quot;effectivesquid.tistory.com&quot; data-og-source-url=&quot;https://effectivesquid.tistory.com/entry/Netty%EC%9D%98-%EA%B8%B0%EB%B3%B8-Component-%EB%B0%8F-Architecture&quot; data-og-url=&quot;https://effectivesquid.tistory.com/entry/Netty%EC%9D%98-%EA%B8%B0%EB%B3%B8-Component-%EB%B0%8F-Architecture&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/igbe4/hyPLj8WPcy/T0RcsOVieQwc3o35D3iEzk/img.png?width=800&amp;amp;height=863&amp;amp;face=0_0_800_863,https://scrap.kakaocdn.net/dn/hvAoa/hyPLjHSVwe/wT56k88p6urK9jb4cHxkH0/img.png?width=800&amp;amp;height=863&amp;amp;face=0_0_800_863,https://scrap.kakaocdn.net/dn/bND2wi/hyPJ5EtyBX/IsJqyzMIwuwCyuumJFg991/img.png?width=1066&amp;amp;height=1150&amp;amp;face=0_0_1066_1150&quot;&gt;&lt;a href=&quot;https://effectivesquid.tistory.com/entry/Netty%EC%9D%98-%EA%B8%B0%EB%B3%B8-Component-%EB%B0%8F-Architecture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://effectivesquid.tistory.com/entry/Netty%EC%9D%98-%EA%B8%B0%EB%B3%B8-Component-%EB%B0%8F-Architecture&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/igbe4/hyPLj8WPcy/T0RcsOVieQwc3o35D3iEzk/img.png?width=800&amp;amp;height=863&amp;amp;face=0_0_800_863,https://scrap.kakaocdn.net/dn/hvAoa/hyPLjHSVwe/wT56k88p6urK9jb4cHxkH0/img.png?width=800&amp;amp;height=863&amp;amp;face=0_0_800_863,https://scrap.kakaocdn.net/dn/bND2wi/hyPJ5EtyBX/IsJqyzMIwuwCyuumJFg991/img.png?width=1066&amp;amp;height=1150&amp;amp;face=0_0_1066_1150');&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;Netty의 기본 Component 및 Architecture&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Netty는 네트워크적인 low한 처리와 비즈니스 로직 처리를 추상화를 통해 분리하였으며 덕분에 비즈니스 로직에 더욱 집중할 수 있도록 도와준다. Netty는 크게 다음과 같은 Component들을 통해 데이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;effectivesquid.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;WebClient vs RestTemplate&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClient는 reactor-netty의 HttpClient를 이용하는 spring 5에서 RestTemplate을 대체하기 위한 component이다. asynchronous &amp;amp; non blocking을 지원한다. WebFlux를 사용하면서 모든 thread를 block시키지 않고 network I/O 처리를 하려면 WebClient를 쓰는 것이 좋다. RestTemplate을 통해서도 Server의 thread를 block시키지 않고 network I/O를 처리 할 수 있다. 위에서 언급한 것처럼 별도의 thread pool에 task를 던져놓고 Server의 thread로 바로 return 해주면 된다. 하지만 thread pool의 thread는 block이 되기 때문에 모든 thread가 non blocking이 되는 모델은 아니다. Server의 thread를 block 시키는 대신 다른 thread를 block 시키는 것 뿐이다. thread pool의 thread가 모두 busy상태이고 queue까지 다 차버렸다면 Server thread는 block될 것이다. node.js의 event loop 모델의 문제점이 webflux에도 똑같이 있는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Test&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 실험을 해보자. 실험을 위해 다음과 같은 준비를 해야한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;api를 호출하면 query param으로 들어오는 delay 값 만큼 sleep 후 응답을 주는 간단한 api server를 하나 만든다.&lt;/li&gt;
&lt;li&gt;WebFlux를 실행시키는 event loop의 worker의 thread 수를 1로 설정한다.&lt;/li&gt;
&lt;li&gt;WebClient를 준비하는데, WebClient를 실행시키는 event loop의 worker thread 수를 1로 설정한다.&lt;/li&gt;
&lt;li&gt;RestTemplate을 준비한다. Mono로 wrapping하며 subscribeOn에 사용할 scheduler의 thread 수를 1로 설정한다.&lt;/li&gt;
&lt;li&gt;WebFlux를 호출하면 WebClient와 RestTemplate을 각각 이용하여 delay api server를 호출하도록한다.&lt;/li&gt;
&lt;li&gt;visualvm을 통해 thread를 분석한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 ~ 5번에 해당하는 코드는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot; data-reactroot=&quot;&quot;&gt;Delay Api Server&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1662962456537&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
class Api {

    @GetMapping(&quot;/**&quot;)
    fun allListen(request: HttpServletRequest, @RequestParam(&quot;delay&quot;) delay: Long): String {
        println(&quot;------------------------------------&quot;)
        println(&quot;listen at: ${LocalDateTime.now()}&quot;)
        println(&quot;path: ${request.servletPath}&quot;)
        println(&quot;query param: ${request.queryString}&quot;)
        println(&quot;thread name: ${Thread.currentThread().name} id: ${Thread.currentThread().id}&quot;)
        println(&quot;------------------------------------&quot;)
        Thread.sleep(delay)
        return &quot;OK&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot; data-reactroot=&quot;&quot;&gt;WebFlux Config&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1662962485751&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class ServerConfig {

    @Bean
    fun reactiveWebServerFactory(): ReactiveWebServerFactory {
        val factory = NettyReactiveWebServerFactory()
        factory.addServerCustomizers(NettyServerCustomizer { it.runOn(LoopResources.create(&quot;webflux&quot;, 1, true)) })

        return factory
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot; data-reactroot=&quot;&quot;&gt;WebClient &amp;amp; RestTemplate Config&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1662962515634&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
class HttpClientConfig {

    @Bean
    fun webClient(): WebClient {
        val httpClient = HttpClient.create()
            .doOnRequest { _, _ -&amp;gt; println(&quot;WebClient doOnRequest ${Thread.currentThread().name} : ${Thread.currentThread().id}&quot;) }
            .doAfterRequest { _, _ -&amp;gt; println(&quot;WebClient doAfterRequest ${Thread.currentThread().name} : ${Thread.currentThread().id}&quot;) }
            .doOnResponse { _, _ -&amp;gt; println(&quot;WebClient doOnResponse ${Thread.currentThread().name} : ${Thread.currentThread().id}&quot;) }
            .runOn(LoopResources.create(&quot;webclient&quot;, 1, true))
            .followRedirect(true)
        val reactorClientHttpConnector = ReactorClientHttpConnector(httpClient)

        return WebClient.builder()
            .clientConnector(reactorClientHttpConnector)
            .build()
    }

    @Bean
    fun restTemplate(): RestTemplate {
        val httpClient = HttpClientBuilder.create()
            .setRedirectStrategy(LaxRedirectStrategy())
            .setMaxConnTotal(10)
            .setMaxConnPerRoute(10)
            .build()

        val factory = HttpComponentsClientHttpRequestFactory()
        factory.httpClient = httpClient

        return RestTemplateBuilder()
            .requestFactory { factory }
            .additionalInterceptors(RestTemplateLoggingRequestInterceptor())
            .build()
    }

    class RestTemplateLoggingRequestInterceptor : ClientHttpRequestInterceptor {

        override fun intercept(
            request: HttpRequest,
            body: ByteArray,
            execution: ClientHttpRequestExecution,
        ): ClientHttpResponse {
            println(&quot;RestTemplate doOnRequest ${Thread.currentThread().name} : ${Thread.currentThread().id}&quot;)
            val response = execution.execute(request, body);
            println(&quot;RestTemplate doOnResponse ${Thread.currentThread().name} : ${Thread.currentThread().id}&quot;)

            return response
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 thread에서 실행하는지 확인하기위해 http 호출 전후의 thread들을 print하도록 설정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot; data-reactroot=&quot;&quot;&gt;Delay Controller &amp;amp; DelayClient&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1662962544974&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
class DelayApi(private val delayClient: DelayClient) {

    @GetMapping(&quot;/api/delay-async&quot;)
    fun delayAsync(@RequestParam(&quot;delay&quot;) delay: Long): Mono&amp;lt;String&amp;gt; {
        return delayClient.invokeAsync(delay)
    }

    @GetMapping(&quot;/api/delay&quot;)
    fun delay(@RequestParam(&quot;delay&quot;) delay: Long): Mono&amp;lt;String&amp;gt; {
        return delayClient.invoke(delay)
    }
}

@Component
class DelayClient(
    @Value(&quot;\${delay.server.url}&quot;) private val url: String,
    private val webClient: WebClient,
    private val restTemplate: RestTemplate
) {

    private val scheduler = Schedulers.newSingle(&quot;play-delay-client&quot;)

    fun invokeAsync(delay: Long): Mono&amp;lt;String&amp;gt; {
        val uri: URI = UriComponentsBuilder.fromHttpUrl(url)
            .queryParam(&quot;delay&quot;, delay)
            .build()
            .encode()
            .toUri()


        return webClient.get()
            .uri(uri)
            .exchangeToMono {
                it.bodyToMono(String::class.java)
            }
    }

    fun invoke(delay: Long): Mono&amp;lt;String&amp;gt; {
        val uri: URI = UriComponentsBuilder.fromHttpUrl(url)
            .queryParam(&quot;delay&quot;, delay)
            .build()
            .encode()
            .toUri()

        return Mono.fromCallable {
            restTemplate.getForObject(uri, String::class.java) ?: &quot;EMPTY&quot;
        }
            .subscribeOn(scheduler)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드를 두 개의 bash shell을 이용하여 동시에 실행 시키면 어떤 결과가 나올지 예상해보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Thread Pool의 수가 1인 Scheduler로 wrapping된 RestTemplate으로 delay api server를 동시에 두 번 호출 할 경우&lt;/li&gt;
&lt;li&gt;EventLoop Worker Thread수가 1인 WebClient로 delay api server를 동시에 두 번 호출 할 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1번 case 부터 분석해보자.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1662962601181&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -X GET &quot;http://localhost:8080/api/delay?delay=10000&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;delay param으로 10000ms(10s)를 전달하면 delay api server는 10초동안 sleep하게되어 응답을 10초 뒤에 줄 것이다. 위의 command를 두 개의 shell에서 동시에 실행 시킨후 webflux server와 delay api server에서 print되는 로그를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot; data-reactroot=&quot;&quot;&gt;webflux server log&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-12 오후 12.48.10.png&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/egEQj1/btrLTCTbMMU/B9TkrmGOBq14EvkYfKSDd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/egEQj1/btrLTCTbMMU/B9TkrmGOBq14EvkYfKSDd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/egEQj1/btrLTCTbMMU/B9TkrmGOBq14EvkYfKSDd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FegEQj1%2FbtrLTCTbMMU%2FB9TkrmGOBq14EvkYfKSDd0%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;162&quot; data-filename=&quot;스크린샷 2022-09-12 오후 12.48.10.png&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;delay api server log&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-12 오후 12.46.52.png&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F2NiJ/btrLSuuhrnr/7K4962vOOXSf7Gmfs28SfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F2NiJ/btrLSuuhrnr/7K4962vOOXSf7Gmfs28SfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F2NiJ/btrLSuuhrnr/7K4962vOOXSf7Gmfs28SfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF2NiJ%2FbtrLSuuhrnr%2F7K4962vOOXSf7Gmfs28SfK%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;374&quot; data-filename=&quot;스크린샷 2022-09-12 오후 12.46.52.png&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;616&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot; data-reactroot=&quot;&quot;&gt;thread dump&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-12 오후 2.35.23.png&quot; data-origin-width=&quot;1216&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baGypt/btrLMx6QRHB/vfeZxEBcMaHWx2rgXRTC8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baGypt/btrLMx6QRHB/vfeZxEBcMaHWx2rgXRTC8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baGypt/btrLMx6QRHB/vfeZxEBcMaHWx2rgXRTC8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaGypt%2FbtrLMx6QRHB%2FvfeZxEBcMaHWx2rgXRTC8k%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;1216&quot; height=&quot;864&quot; data-filename=&quot;스크린샷 2022-09-12 오후 2.35.23.png&quot; data-origin-width=&quot;1216&quot; data-origin-height=&quot;864&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot; data-reactroot=&quot;&quot;&gt;&lt;b&gt;webflux server log&lt;/b&gt;를 보면 같은 thread에서 호출을 하는 것을 확인할 수 있으며 &lt;b&gt;delay api server log&lt;/b&gt;를 통해 동시에 호출한 요청이 순차적으로 10초 간격으로 inbound된 것을 확인할 수 있다. 즉, thread가 block이 된 것이고 두 개의 요청을 처리하는데 20초가 걸린 것이다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;thread dump 를 보면 기존에 제공하는 java io를 이용하여 socket read를 기다리는 것을 확인할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 WebClient를 사용하는 &lt;b&gt;2번 case&lt;/b&gt;를 분석해보자. &lt;b&gt;http 요청에 사용하는 thread수는 1개&lt;/b&gt;라는 조건은 똑같다.&lt;/p&gt;
&lt;pre id=&quot;code_1662962743010&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -X GET &quot;http://localhost:8080/api/delay-async?delay=10000&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 endpoint로 webflux server 에 요청하면 webclient를 통해 delay api server를 호출하게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 command를 1번 case 실험과 똑같이 두 shell에서 동시에 실행해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot; data-reactroot=&quot;&quot;&gt;webflux server log&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-12 오후 12.54.55.png&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sttu6/btrLPo89JaM/rRbVpT51mZa3dAJ5BVygh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sttu6/btrLPo89JaM/rRbVpT51mZa3dAJ5BVygh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sttu6/btrLPo89JaM/rRbVpT51mZa3dAJ5BVygh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsttu6%2FbtrLPo89JaM%2FrRbVpT51mZa3dAJ5BVygh0%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;214&quot; data-filename=&quot;스크린샷 2022-09-12 오후 12.54.55.png&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot; data-reactroot=&quot;&quot;&gt;delay api server log&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-12 오후 12.58.17.png&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXajwI/btrLMxsffT4/GiKwPFnGWamSIHb1YNGl80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXajwI/btrLMxsffT4/GiKwPFnGWamSIHb1YNGl80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXajwI/btrLMxsffT4/GiKwPFnGWamSIHb1YNGl80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXajwI%2FbtrLMxsffT4%2FGiKwPFnGWamSIHb1YNGl80%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;391&quot; data-filename=&quot;스크린샷 2022-09-12 오후 12.58.17.png&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot; data-reactroot=&quot;&quot;&gt;thread dump&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-12 오후 2.37.46.png&quot; data-origin-width=&quot;1127&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC2TQD/btrLMzXYVQ3/EZAl3UHKkVYWiLgcPfLkxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC2TQD/btrLMzXYVQ3/EZAl3UHKkVYWiLgcPfLkxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC2TQD/btrLMzXYVQ3/EZAl3UHKkVYWiLgcPfLkxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC2TQD%2FbtrLMzXYVQ3%2FEZAl3UHKkVYWiLgcPfLkxk%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;1127&quot; height=&quot;348&quot; data-filename=&quot;스크린샷 2022-09-12 오후 2.37.46.png&quot; data-origin-width=&quot;1127&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 case와는 다른 양상을 볼 수 있다. &lt;b&gt;webflux server log는 1번 case와 같이 하나의 thread에서 실행하지만 request &amp;amp; response 로그가 순차적으로 찍히지 않았다. 이에더해 delay api server log를 보면 요청이 동시에 inbound 된 것을 확인할 수 있다.&lt;/b&gt; 즉, thread가 block이 되지 않았고 두 개의 요청을 처리하는데 &lt;b&gt;20초가 아닌 10초만 걸렸다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;thread dump 를 보면 java nio를 이용하여 socket read를 기다리지 않는 것을 확인할 수 있다. (sun.nio.ch package는 java nio에서 제공하는 interface를 구현한 실제 구현체가 있는 package이다.)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 실험을 통해 WebClient를 사용하는 WebFlux는 모든 thread를 block시키지 않고 network I/O를 처리 할 수 있다. 물론 jdbc와 같이 아직까지 non blocking을 지원하지 않는 도구를 이용한다면 1번 case와 같이 별도의 thread pool로 task를 던져버리고 server thread만이라도 block이 되지 않도록 해야한다. 하지만 적어도 network I/O 만큼은 thread를 block시키지 않게 개발한다면 cpu를 좀 더 효율적으로 사용할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 글을 작성하다보니 글이 길어진 것 같다. 테스트 관련 글은 별도의 글로 포스팅하는 것을 고민하다가 한번에 살펴보시는게 좋을 것 같아 한번에 작성하게 되었다. 해당내용에 잘못된 내용이 있다면 댓글에 악플이라도 달아주었으면 좋겠다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 글:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;http://eincs.com/2009/08/java-nio-bytebuffer-channel-file/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://eincs.com/2009/08/java-nio-bytebuffer-channel-file/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1662964191142&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;[NIO] JAVA NIO의 ByteBuffer와 Channel로 File Handling에서 더 좋은 Perfermance 내기!&quot; data-og-description=&quot;기존의 Java IO는 다른 언어에 비해 매우 느리다는 이야기가 많이 있습니다. 내부적으로 어떻게 돌아가는지 대략적으로나마 파악한다면 그럴 수 밖에 없었다는 사실을 알게 되실겁니다. 하지만 jd&quot; data-og-host=&quot;eincs.com&quot; data-og-source-url=&quot;http://eincs.com/2009/08/java-nio-bytebuffer-channel-file/&quot; data-og-url=&quot;http://eincs.com/2009/08/java-nio-bytebuffer-channel-file/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/LRqtf/hyPLj2ci2b/0kEx5gjI5uKZk05uf1BMB1/img.jpg?width=300&amp;amp;height=223&amp;amp;face=0_0_300_223,https://scrap.kakaocdn.net/dn/cDnR2O/hyPJ4lgRcg/bwKx4UrMt1opf2nuOKZvpk/img.jpg?width=640&amp;amp;height=233&amp;amp;face=0_0_640_233,https://scrap.kakaocdn.net/dn/bVnaT8/hyPJYL8aau/D8BdJZV5mbFTwJFO1UBDm0/img.jpg?width=448&amp;amp;height=321&amp;amp;face=0_0_448_321&quot;&gt;&lt;a href=&quot;http://eincs.com/2009/08/java-nio-bytebuffer-channel-file/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;http://eincs.com/2009/08/java-nio-bytebuffer-channel-file/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/LRqtf/hyPLj2ci2b/0kEx5gjI5uKZk05uf1BMB1/img.jpg?width=300&amp;amp;height=223&amp;amp;face=0_0_300_223,https://scrap.kakaocdn.net/dn/cDnR2O/hyPJ4lgRcg/bwKx4UrMt1opf2nuOKZvpk/img.jpg?width=640&amp;amp;height=233&amp;amp;face=0_0_640_233,https://scrap.kakaocdn.net/dn/bVnaT8/hyPJYL8aau/D8BdJZV5mbFTwJFO1UBDm0/img.jpg?width=448&amp;amp;height=321&amp;amp;face=0_0_448_321');&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;[NIO] JAVA NIO의 ByteBuffer와 Channel로 File Handling에서 더 좋은 Perfermance 내기!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;기존의 Java IO는 다른 언어에 비해 매우 느리다는 이야기가 많이 있습니다. 내부적으로 어떻게 돌아가는지 대략적으로나마 파악한다면 그럴 수 밖에 없었다는 사실을 알게 되실겁니다. 하지만 jd&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;eincs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://palpit.tistory.com/643&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://palpit.tistory.com/643&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1662964252648&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;[Java] NIO 기반 입출력 및 네트워킹 - 파일 비동기 채널&quot; data-og-description=&quot;NIO 기반 입출력 및 네트워킹은 여러 절로 구성되어 있습니다. [Java] NIO 기반 입출력 및 네트워킹 - NIO, 파일 &amp;amp; 디렉토리 [Java] NIO 기반 입출력 및 네트워킹 - 버퍼(Buffer) [Java] NIO 기반 입출력 및 네트&quot; data-og-host=&quot;palpit.tistory.com&quot; data-og-source-url=&quot;https://palpit.tistory.com/643&quot; data-og-url=&quot;https://palpit.tistory.com/entry/Java-NIO-%EA%B8%B0%EB%B0%98-%EC%9E%85%EC%B6%9C%EB%A0%A5-%EB%B0%8F-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%ED%8C%8C%EC%9D%BC-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B1%84%EB%84%90&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bWXziZ/hyPLmYVfTj/AZq7UfeR21TSaZ800yuLE0/img.png?width=728&amp;amp;height=397&amp;amp;face=0_0_728_397,https://scrap.kakaocdn.net/dn/lfLA7/hyPLh4mN8H/aWag8qfgFYk7XZcfwMk5OK/img.png?width=728&amp;amp;height=397&amp;amp;face=0_0_728_397,https://scrap.kakaocdn.net/dn/kXzln/hyPJUJIXMZ/UhKh90g7hx3ClTkEWY67X1/img.png?width=728&amp;amp;height=497&amp;amp;face=0_0_728_497&quot;&gt;&lt;a href=&quot;https://palpit.tistory.com/643&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://palpit.tistory.com/643&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bWXziZ/hyPLmYVfTj/AZq7UfeR21TSaZ800yuLE0/img.png?width=728&amp;amp;height=397&amp;amp;face=0_0_728_397,https://scrap.kakaocdn.net/dn/lfLA7/hyPLh4mN8H/aWag8qfgFYk7XZcfwMk5OK/img.png?width=728&amp;amp;height=397&amp;amp;face=0_0_728_397,https://scrap.kakaocdn.net/dn/kXzln/hyPJUJIXMZ/UhKh90g7hx3ClTkEWY67X1/img.png?width=728&amp;amp;height=497&amp;amp;face=0_0_728_497');&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;[Java] NIO 기반 입출력 및 네트워킹 - 파일 비동기 채널&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;NIO 기반 입출력 및 네트워킹은 여러 절로 구성되어 있습니다. [Java] NIO 기반 입출력 및 네트워킹 - NIO, 파일 &amp;amp; 디렉토리 [Java] NIO 기반 입출력 및 네트워킹 - 버퍼(Buffer) [Java] NIO 기반 입출력 및 네트&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;palpit.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/non-blocking-server-in-java-nio/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.geeksforgeeks.org/non-blocking-server-in-java-nio/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1662964257679&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;Non Blocking Server in Java NIO - GeeksforGeeks&quot; data-og-description=&quot;A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.&quot; data-og-host=&quot;www.geeksforgeeks.org&quot; data-og-source-url=&quot;https://www.geeksforgeeks.org/non-blocking-server-in-java-nio/&quot; data-og-url=&quot;https://www.geeksforgeeks.org/non-blocking-server-in-java-nio/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/N7lCb/hyPJUbSkM3/o5aQltO61KmP2dOfrvdJw1/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/bUlebc/hyPLi93hWz/37sn7YiRZeBrCOuoQqK6Qk/img.png?width=600&amp;amp;height=400&amp;amp;face=0_0_600_400,https://scrap.kakaocdn.net/dn/BwJCx/hyPJZqIUup/7j8kD7lyOtfev2JZnLKmD1/img.png?width=600&amp;amp;height=400&amp;amp;face=0_0_600_400&quot;&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/non-blocking-server-in-java-nio/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.geeksforgeeks.org/non-blocking-server-in-java-nio/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/N7lCb/hyPJUbSkM3/o5aQltO61KmP2dOfrvdJw1/img.png?width=200&amp;amp;height=200&amp;amp;face=0_0_200_200,https://scrap.kakaocdn.net/dn/bUlebc/hyPLi93hWz/37sn7YiRZeBrCOuoQqK6Qk/img.png?width=600&amp;amp;height=400&amp;amp;face=0_0_600_400,https://scrap.kakaocdn.net/dn/BwJCx/hyPJZqIUup/7j8kD7lyOtfev2JZnLKmD1/img.png?width=600&amp;amp;height=400&amp;amp;face=0_0_600_400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Non Blocking Server in Java NIO - GeeksforGeeks&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.geeksforgeeks.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>Framework/Spring</category>
      <author>코딩하는 오징어</author>
      <guid isPermaLink="true">https://effectivesquid.tistory.com/80</guid>
      <comments>https://effectivesquid.tistory.com/entry/WebFlux-%EC%82%AC%EC%9A%A9%EC%8B%9C-WebClient%EB%A5%BC-%EC%8D%A8%EC%95%BC%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0#entry80comment</comments>
      <pubDate>Mon, 12 Sep 2022 15:10:41 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes 컨테이너 환경에서의 컴퓨팅 리소스 정보 및 제한</title>
      <link>https://effectivesquid.tistory.com/entry/Kubernetes-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EC%BB%B4%ED%93%A8%ED%8C%85-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EC%A0%95%EB%B3%B4-%EB%B0%8F-%EC%A0%9C%ED%95%9C</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;리소스 할당 (Cpu &amp;amp; Memory)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Pod안에서 실행되는 컨테이너가 사용하는 리소스를 제한하지 않으면 Node의 리소스가 무분별하게 사용될 수 있다. 여러 Pod이 존재한다면 Qos 클래스(BestEffort, Burstable, Guaranteed)에 따라 필요한 만큼의 리소스를 얻지 못할 수도 있다. requests, limits를 모두 설정하지 않았다면 리소스 할당 순위에서 뒤로 밀려 실행중인 Pod이 kill 당할 수도 있다. Pod에 리소스 설정을 하는 방법은 간단하다. 다음과 같이 template에 requests와 limits를 설정하면 된다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
  name: play-k8s-app
spec:
  containers:
  - name: play-app
    image: {image url}
    resources:
      requests:
        memory: &quot;256Mi&quot;
        cpu: &quot;1&quot;
      limits:
        memory: &quot;256Mi&quot;
        cpu: &quot;1&quot;
  - name: log-aggregator
    image: {image url}
    resources:
      requests:
        memory: &quot;64Mi&quot;
        cpu: &quot;250m&quot;
      limits:
        memory: &quot;128Mi&quot;
        cpu: &quot;500m&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 할당은 Pod에 하는 것이 아닌 Pod에서 실행되는 각 컨테이너에 할당하는 것이다. 이 점을 주의해야한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;컨테이너에서 실행되는 Application에서의 리소스 정보&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-컨테이너는-항상-컨테이너-메모리가-아닌-node의-메모리를-본다&quot; href=&quot;https://github.com/gksxodnd007/TIL/blob/master/k8s/resource-limit-jvm.md#%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EB%8A%94-%ED%95%AD%EC%83%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%A9%94%EB%AA%A8%EB%A6%AC%EA%B0%80-%EC%95%84%EB%8B%8C-node%EC%9D%98-%EB%A9%94%EB%AA%A8%EB%A6%AC%EB%A5%BC-%EB%B3%B8%EB%8B%A4&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;&lt;b&gt;컨테이너는 항상 컨테이너 메모리가 아닌 Node의 메모리를 본다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Pod를 실행시킨후 Pod에서 실행되는 컨테이너의 접속하여 top 명령을 실행시키면 사용가능한 메모리양은 컨테이너가 실행 중인 노드의 전체 메모리양을 표시한다. 컨테이너에 사용 가능한 메모리의 제한을 설정하더라도 컨테이너는 이 제한을 인식하지 못한다. 이는 시스템에서 사용 가능한 메모리 양을 조회하고 해당 정보를 사용해 예약하려는 메모리 양을 결정하는 모든 애플리케이션에 좋지 않은 영향을 미친다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-컨테이너는-항상-컨테이너가-사용가능한-cpu-사용량이-아닌-노드의-cpu-자원을-본다&quot; href=&quot;https://github.com/gksxodnd007/TIL/blob/master/k8s/resource-limit-jvm.md#%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EB%8A%94-%ED%95%AD%EC%83%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EA%B0%80-%EC%82%AC%EC%9A%A9%EA%B0%80%EB%8A%A5%ED%95%9C-cpu-%EC%82%AC%EC%9A%A9%EB%9F%89%EC%9D%B4-%EC%95%84%EB%8B%8C-%EB%85%B8%EB%93%9C%EC%9D%98-cpu-%EC%9E%90%EC%9B%90%EC%9D%84-%EB%B3%B8%EB%8B%A4&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;&lt;b&gt;컨테이너는 항상 컨테이너가 사용가능한 CPU 사용량이 아닌 노드의 CPU 자원을 본다.&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;메모리와 마찬가지로 컨테이너는 컨테이너에 설정된 CPU 제한과 상관없이 노드의 모든 CPU를 본다. CPU 제한을 1코어로 설정한다고 컨테이너에 CPU 1코어만을 노출하지 않는다. CPU 제한이 하는 일은 컨테이너가 사용할 수 있는 CPU 시간의 양을 제한하는 것이다. 64코어 CPU에 실행중인 1코어 CPU 제한의 컨테이너는 전체 CPU시간의 1/64을 얻는다. CPU 제한이 1코어로 설정되더라도 컨테이너의 프로세스는 한 개 코어에서만 실행되는 것이 아니다. 다른 시점에서 다른 코어에서 코드가 실행될 수 있다. 어떤 애플리케이션은 시스템의 CPU 수를 검색해 실행해야 할 작업 스레드 수를 결정한다. Java의 ForkJoinPool은 default로 PC의 CPU core수로 pool의 thread수를 설정한다. 코드를 보면 생성자에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;Runtime.getRuntime().availableProcessors()를 호출하는 것을 확인할 수 있다. 이런 상황에서는 노드의 spec에 따라 값이 달라지므로 스케줄링 되는 노드에 따라 애플리케이션에 문제가 생길수도 있고 잘 동작할 수도 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-컨테이너에서-실행되는-java-application에서-바라보는-리소스-정보&quot; href=&quot;https://github.com/gksxodnd007/TIL/blob/master/k8s/resource-limit-jvm.md#%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%97%90%EC%84%9C-%EC%8B%A4%ED%96%89%EB%90%98%EB%8A%94-java-application%EC%97%90%EC%84%9C-%EB%B0%94%EB%9D%BC%EB%B3%B4%EB%8A%94-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EC%A0%95%EB%B3%B4&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;&lt;b&gt;컨테이너에서 실행되는 Java Application에서 바라보는 리소스 정보&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Java 8u191 버전까지는 컨테이너의 limit정보를 읽지 못해 heap size를 컨테이너에 할당된 memory 정보가 아닌 host의 memory 정보를 보고 Xmx(max heap size)값을 설정하였다. 물론 해당 값을 수동으로 주게되면 문제가 없지만, 해당 값을 별도로 설정하지 않으면 default로 Java Application이 바라보는 memory 정보의 1/4로 설정하게 된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;Runtime.getRuntime().availableProcessors()를 통해 읽어오는 값도 컨테이너에 할당된 cpu를 바라보는 것이 아닌 host의 cpu정보를 읽어오게된다. Java 8u191 버전 이후 즉, Java 8u192 버전 부터는 JVM이&lt;span&gt;&amp;nbsp;&lt;/span&gt;cgroup limits&lt;span&gt;&amp;nbsp;&lt;/span&gt;정보를 읽어올 수 있게되어 컨테이너에 부여된 리소스 정보를 바라보게 되었다. JVM 옵션인&lt;span&gt;&amp;nbsp;&lt;/span&gt;-XX:+UseContainerSupport를 주면되고 해당 옵션은 default로 enable 되어있다. 컨테이너의 리소스 정보가 아닌 host의 리소스 정보를 보게하고 싶다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;-XX:-UseContainerSupport를 주면 된다. (+가아닌 -를 주면 됨) JVM이 컨테이너에 설정된 리소스 정보를 잘 보는지 간단하게 테스트해보겠다. spring web mvc framework를 이용하여 현재 애플리케이션의 정보를 응답으로 내려주는 API를 개발하였다. 그 후 빌드 된 jar를 이용하여 docker image를 만들어 kubernetes cluster에서 Pod으로 실행시켜 테스트하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Resource Info API&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
public class ResourceChecker {

    private static final ResourceInfo RESOURCE_INFO = new ResourceInfo();

    @GetMapping(&quot;/api/v1/resource&quot;)
    public ResourceInfo resource() {
        return RESOURCE_INFO;
    }

    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
    @Getter
    public static class ResourceInfo {

        private final int cpus;
        private final String totalMemory;
        private final String maxMemory;
        private final String javaVersion;

        public ResourceInfo() {
            this.cpus = Runtime.getRuntime().availableProcessors();
            this.totalMemory = toMb(Runtime.getRuntime().totalMemory()) + &quot;MB&quot;;
            this.maxMemory = toMb(Runtime.getRuntime().maxMemory()) + &quot;MB&quot;;
            this.javaVersion = ManagementFactory.getRuntimeMXBean().getVmVersion();
        }

        private static long toMb(long memorySize) {
            return memorySize / (1000 * 1000);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-XX:[+|-]UseContainerSupport&lt;span&gt;&amp;nbsp;&lt;/span&gt;JVM 옵션에따라 애플리케이션이 바라보는 리소스 정보를 살펴보자. 먼저 실행환경은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1644056748640&quot; class=&quot;routeros&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Worker Node Spec
- cpus: 2
- memory: 2G

Container Resource
- requests
  - cpus: 1
  - memory: 1G
- limits
  - cpus: 1
  - memory: 1G&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;479&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yLujA/btrsveki4HE/evsKQlrzttkZNdlAP3Xy10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yLujA/btrsveki4HE/evsKQlrzttkZNdlAP3Xy10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yLujA/btrsveki4HE/evsKQlrzttkZNdlAP3Xy10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyLujA%2Fbtrsveki4HE%2FevsKQlrzttkZNdlAP3Xy10%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;479&quot; height=&quot;365&quot; data-origin-width=&quot;479&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 정보를 토대로 다음 결과를 해석해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;-XX:+UseContainerSupport&lt;span&gt;&amp;nbsp;&lt;/span&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;996&quot; data-origin-height=&quot;188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lg1qs/btrstLo98Mo/PaxoqoIEgYyoN5DK9jGZE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lg1qs/btrstLo98Mo/PaxoqoIEgYyoN5DK9jGZE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lg1qs/btrstLo98Mo/PaxoqoIEgYyoN5DK9jGZE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLg1qs%2FbtrstLo98Mo%2FPaxoqoIEgYyoN5DK9jGZE1%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;996&quot; height=&quot;188&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;188&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;-XX:-UseContainerSupport&lt;span&gt;&amp;nbsp;&lt;/span&gt;옵션을 주었을 경우&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFEkAZ/btrswUZ9wEt/ZaNxF7LhpUHyLzrBsjwxZK/img.png&quot; data-image-src=&quot;https://blog.kakaocdn.net/dn/bFEkAZ/btrswUZ9wEt/ZaNxF7LhpUHyLzrBsjwxZK/img.png&quot; data-origin-width=&quot;993&quot; data-origin-height=&quot;184&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UseContainerSupport&lt;span&gt;&amp;nbsp;&lt;/span&gt;option을 활성화 시켰을 경우에는 jvm이 container에 설정된 리소스 정보를 바라보는 것을 cpus와 max_memory 값을 통해 확인할 수 있다. 반면에&lt;span&gt;&amp;nbsp;&lt;/span&gt;UseContainerSupport&lt;span&gt;&amp;nbsp;&lt;/span&gt;option을 비활 성화 시켰을 경우에는 jvm이 container가 실행중인 host 즉, worker node의 리소스 정보를 바라보는 것을 확인할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-jvm-application이-실행되는-컨테이너에-메모리-제한하기&quot; href=&quot;https://github.com/gksxodnd007/TIL/blob/master/k8s/resource-limit-jvm.md#jvm-application%EC%9D%B4-%EC%8B%A4%ED%96%89%EB%90%98%EB%8A%94-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%97%90-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%A0%9C%ED%95%9C%ED%95%98%EA%B8%B0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;&lt;b&gt;JVM Application이 실행되는 컨테이너에 메모리 제한하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 까지는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Xmx,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Xms값을 별도로 설정하지 않고 default값으로 주어 테스트를 진행하였다. 하지만 대부분의 운영 애플리케이션은 max heap size와 min heap size를 설정하여 실행시킨다. 그렇다면 max heap size만큼만 container가 메모리를 사용할 수 있도록 하면 될까? 다시 말하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;-Xmx2G로 설정하여 jvm 애플리케이션을 실행시켰다면 container의 memory limit도 2G로 설정하면 되는걸까? 정답은 '아니오'이다. 왜 아닌지 다음 테스트를 통해 살펴보자. 테스트 시나리오는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Xmx1G,&amp;nbsp;Xms1G&amp;nbsp;option을 주어 jvm의 max heap size를 1G로 설정한다.&lt;/li&gt;
&lt;li&gt;container의 memory resource limit을 1G로 설정한다.&lt;/li&gt;
&lt;li&gt;jvm이 heap memory를 1G만큼 사용하는지 확인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 시나리오를 테스트하기 위해 memory usage를 증가 시키는 API를 제공하는 코드를 작성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Increase Memory Usage API&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;@Slf4j
@RestController
public class OutOfMemeoryExceptionTrigger {

    private static final List&amp;lt;Object&amp;gt; LIST = new ArrayList&amp;lt;&amp;gt;();

    @GetMapping(&quot;/api/v1/store&quot;)
    public String store(@RequestParam(&quot;count&quot;) int count) {
        try {
            IntStream.range(0, count)
                    .forEach(idx -&amp;gt; LIST.add(new Object()));
        } catch (Throwable e) {
            log.error(&quot;LIST SIZE: {}&quot;, LIST.size(), e);
        }

        return &quot;LIST SIZE: &quot; + LIST.size();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 query param으로 주어지는 count 수 만큼 static 변수인 LIST에 Object를 추가하는 코드이다. static 변수는 gc 대상이 아니므로 Object가 추가 될 때마다 memory usage를 계속해서 증가 시킬 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pod template&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;...
  spec:
    containers:
    - name: play-app
      image: codingsquid/play-k8s-app:resource-check
      imagePullPolicy: IfNotPresent
      ports:
      - containerPort: 80
      resources:
        limits:
          cpu: &quot;1&quot;
          memory: &quot;1Gi&quot;
        requests:
          cpu: &quot;1&quot;
          memory: &quot;1Gi&quot;
      env:
        - name: SPRING_PROFILES_ACTIVE
          value: develop
        - name: JAVA_OPTS
          value: &quot;-XX:+UseContainerSupport -Xmx1G -Xms1G&quot;
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 yaml은 pod의 일부분만 가져왔다. JAVA_OPTS를 보면&lt;span&gt;&amp;nbsp;&lt;/span&gt;Xmx&lt;span&gt;&amp;nbsp;&lt;/span&gt;값을 1G로 설정한 것을 확인할 수 있다. 위와 같은 환경으로 memory usage를 증가시키는 api를 호출하여 테스트를 진행했을 때 다음과 같은 결과를 얻을 수 있었다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;curl -X GET &quot;http://play-k8s-app.dev.kakao.com/play/api/v1/store?count={count}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pod의 describe&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;747&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYGNXI/btrstJ5YbDQ/MqC0kuDkNVx8FO8UaeAs20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYGNXI/btrstJ5YbDQ/MqC0kuDkNVx8FO8UaeAs20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYGNXI/btrstJ5YbDQ/MqC0kuDkNVx8FO8UaeAs20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYGNXI%2FbtrstJ5YbDQ%2FMqC0kuDkNVx8FO8UaeAs20%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;762&quot; height=&quot;747&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;747&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pod의 container에서 실행시킨 top 명령어 결과 (RES: 실제 사용중인 메모리 크기)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;839&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mnoib/btrsytne7XR/4V3V228ef0rQjksYfURYR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mnoib/btrsytne7XR/4V3V228ef0rQjksYfURYR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mnoib/btrsytne7XR/4V3V228ef0rQjksYfURYR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmnoib%2Fbtrsytne7XR%2F4V3V228ef0rQjksYfURYR0%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;839&quot; height=&quot;265&quot; data-origin-width=&quot;839&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jvm이 heap memory를 1G를 다 사용하지 않았음에도 불구하고 OOMKilled가 발생하여 Pod이 restart되었다. 당연한 결과이지만 종종 놓치는 경우가 있다. jvm이 프로세스로 실행되어 메모리에 로딩될 때 heap 영역만 있는 것이 아니다. code cache, thread stack, gc datastructure 등의 non heap 영역도 존재한다. 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;container에 리소스 제한을 설정할 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;Xmx으로 설정된 max heap size보다 좀 더 크게 설정해주어야한다. 그렇지 않으면 jvm이 컨테이너에 할당된 메모리보다 더 많이 사용하려고 시도하게되고 그렇게되면 Pod에 OOMKilled가 발생할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kubernetes In Action&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.atamanroman.dev/articles/usecontainersupport-to-the-rescue&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.atamanroman.dev/articles/usecontainersupport-to-the-rescue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8146115&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8146115&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://srvaroa.github.io/jvm/kubernetes/memory/docker/oomkiller/2019/05/29/k8s-and-java.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://srvaroa.github.io/jvm/kubernetes/memory/docker/oomkiller/2019/05/29/k8s-and-java.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.oracle.com/java/technologies/javase/8u191-relnotes.html#JDK-8146115&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.oracle.com/java/technologies/javase/8u191-relnotes.html#JDK-8146115&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8157478&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8157478&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://plumbr.io/blog/memory-leaks/why-does-my-java-process-consume-more-memory-than-xmx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://plumbr.io/blog/memory-leaks/why-does-my-java-process-consume-more-memory-than-xmx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/ops/docker-jvm-heap-size&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.baeldung.com/ops/docker-jvm-heap-size&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Server/Kubernetes</category>
      <author>코딩하는 오징어</author>
      <guid isPermaLink="true">https://effectivesquid.tistory.com/79</guid>
      <comments>https://effectivesquid.tistory.com/entry/Kubernetes-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EC%BB%B4%ED%93%A8%ED%8C%85-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EC%A0%95%EB%B3%B4-%EB%B0%8F-%EC%A0%9C%ED%95%9C#entry79comment</comments>
      <pubDate>Sat, 5 Feb 2022 19:21:04 +0900</pubDate>
    </item>
    <item>
      <title>VirtualBox를 이용하여 k8s cluster 구성하기</title>
      <link>https://effectivesquid.tistory.com/entry/VirtualBox%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-k8s-cluster-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;환경정보&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OS: macOS Big Sur 11.5.2&lt;/li&gt;
&lt;li&gt;VirtualBox: 6.1.26&lt;/li&gt;
&lt;li&gt;Ubuntu: 20.04.3 LTS Desktop&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-virtualbox에-ubuntu-올리기&quot; href=&quot;https://github.com/gksxodnd007/TIL/blob/master/k8s/virtualbox-cluster.md#virtualbox%EC%97%90-ubuntu-%EC%98%AC%EB%A6%AC%EA%B8%B0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;&lt;b&gt;VirtualBox에 Ubuntu 올리기&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가상머신을 새로 만든 후 적절한 spec을 선택 후 다운로드 한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://ubuntu.com/download/desktop&quot;&gt;ubuntu 이미지&lt;/a&gt;를 설정한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;memory: 2GB, 디스크: 20GB, cpu: 2개&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;네트워크를 어댑터 브리지로 설정한 후 가상 머신을 실행시킨다.&lt;/li&gt;
&lt;li&gt;ubuntu를 설치했다면 필요한 tool들을 설치한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sudo apt-get update&lt;/li&gt;
&lt;li&gt;sudo apt-get install net-tools&lt;/li&gt;
&lt;li&gt;sudo apt-get install vim&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가상머신과 host 사이에 clipboard를 공유하기 위해 다음과 같은 설정을 해준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;VirtualBox에서 가상머신 설정에 일반 -&amp;gt; 고급 -&amp;gt; 클립보드 공유 &amp;amp; 드래그 앤 드롭 -&amp;gt; 양방향으로 설정&lt;/li&gt;
&lt;li&gt;가상머신에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;sudo apt-get install virtualbox-guest-x11을 실행한다.&lt;/li&gt;
&lt;li&gt;설치가 되었다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;VBoxClient --clipboard를 실행한다.&lt;/li&gt;
&lt;li&gt;가상 머신에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;ctrl + shift + c, ctrl + shift + v, host에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;command + c, command + v를 이용하여 복사, 붙여넣기를 이용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-virtualbox에-kubeadm-설치하기&quot; href=&quot;https://github.com/gksxodnd007/TIL/blob/master/k8s/virtualbox-cluster.md#virtualbox%EC%97%90-kubeadm-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;&lt;b&gt;VirtualBox에 kubeadm 설치하기&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;docs:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://kubernetes.io/ko/docs/setup/production-environment/tools/kubeadm/install-kubeadm/&quot;&gt;https://kubernetes.io/ko/docs/setup/production-environment/tools/kubeadm/install-kubeadm/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. apt 패키지 색인을 업데이트하고, 쿠버네티스 apt 리포지터리를 사용하는 데 필요한 패키지를 설치한다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;구글 클라우드의 공개 사이닝 키를 다운로드 한다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;쿠버네티스 apt 리포지터리를 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;echo &quot;deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main&quot; | sudo tee /etc/apt/sources.list.d/kubernetes.list
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. apt 패키지 색인을 업데이트하고, kubelet, kubeadm, kubectl을 설치하고 해당 버전을 고정한다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-virtualbox에서-kubernetes-cluster-구성하기&quot; href=&quot;https://github.com/gksxodnd007/TIL/blob/master/k8s/virtualbox-cluster.md#virtualbox%EC%97%90%EC%84%9C-kubernetes-cluster-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;&lt;b&gt;VirtualBox에서 kubernetes cluster 구성하기&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위의 과정에서 생성된 가상머신을 두 개 (worker1, worker2) 복제한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;1. MAC 주소 정책을&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;모든 네트워크 어댑터의 새 MAC 주소 생성으로 선택해야한다. 그렇지 않으면 ip가 master와 같게 설정되어 네트워크 연결이 되지 않는다. (ip 부여는 MAC주소와 연관되기 때문)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SCUaN/btrn16bWRCR/8WR1cBGgDLvzZ4KAJo8YS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SCUaN/btrn16bWRCR/8WR1cBGgDLvzZ4KAJo8YS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SCUaN/btrn16bWRCR/8WR1cBGgDLvzZ4KAJo8YS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSCUaN%2Fbtrn16bWRCR%2F8WR1cBGgDLvzZ4KAJo8YS0%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;644&quot; height=&quot;366&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 완전한 복제를 선택하여 복제를 진행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/stFpt/btrn0jcieII/KimC4ZmVmOOrpxRUgfPd6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/stFpt/btrn0jcieII/KimC4ZmVmOOrpxRUgfPd6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/stFpt/btrn0jcieII/KimC4ZmVmOOrpxRUgfPd6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FstFpt%2Fbtrn0jcieII%2FKimC4ZmVmOOrpxRUgfPd6k%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;639&quot; height=&quot;364&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 구성된 vm들은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqDYLf/btrnWSsM1IH/mM8P2kW43XEckitDzSvPx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqDYLf/btrnWSsM1IH/mM8P2kW43XEckitDzSvPx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqDYLf/btrnWSsM1IH/mM8P2kW43XEckitDzSvPx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqDYLf%2FbtrnWSsM1IH%2FmM8P2kW43XEckitDzSvPx1%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;684&quot; height=&quot;457&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;457&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. hostname을 모두 각 host의 맞게 변경한 후 reboot한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo vim /etc/hostname&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 도구를 클릭하고 환경 설정 버튼을 클릭한 후 네트워크 탭을 누른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 새 NAT 네트워크 생성을 통해 네트워크를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 각각의 vm들 (k8s-master, k8s-worker1, k8s-worker2, k8s-worker3)의 설정을 열어 네트워크를 클릭하고 어댑터1의 속성을 아래와 같이 변경한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;337&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0N6yR/btrn16iH3pD/Qy9NA59ibdntUuKttA9Un1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0N6yR/btrn16iH3pD/Qy9NA59ibdntUuKttA9Un1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0N6yR/btrn16iH3pD/Qy9NA59ibdntUuKttA9Un1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0N6yR%2Fbtrn16iH3pD%2FQy9NA59ibdntUuKttA9Un1%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;676&quot; height=&quot;337&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;337&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. 각각의 vm들을 실행한 후 터미널에 ifconfig or ip addr을 입력하면 ip가 설정되어있다. vm에서 해당 ip로 ping을 날려 정상적으로 수행되는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9. virtualbox를 이용한 k8s 클러스터의 구성은 다음과 같다고 볼 수 있다.&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;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjtG0d/btrn1zrU8LJ/QRB3CApk9MDYCEWLJED850/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjtG0d/btrn1zrU8LJ/QRB3CApk9MDYCEWLJED850/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjtG0d/btrn1zrU8LJ/QRB3CApk9MDYCEWLJED850/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjtG0d%2Fbtrn1zrU8LJ%2FQRB3CApk9MDYCEWLJED850%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;504&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-kubernetes-노드-추가&quot; href=&quot;https://github.com/gksxodnd007/TIL/blob/master/k8s/virtualbox-cluster.md#kubernetes-%EB%85%B8%EB%93%9C-%EC%B6%94%EA%B0%80&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;&lt;b&gt;kubernetes 노드 추가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 각 vm에서 다음과 같이 터미널로 swap 기능을 꺼준다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;sudo -i
swapoff -a
sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. kubeadm init 실행한다.&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&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;kubeadm join &amp;lt;Kubernetes API Server:PORT&amp;gt; --token &amp;lt;Token 값&amp;gt; --discovery-token-ca-cert-hash sha256:&amp;lt;Hash 값&amp;gt;&lt;/b&gt;이 출력된다. worker 노드를 cluster에 join할 때 사용해야하므로 잘 저장해둔다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;kubeadm init&lt;/b&gt; 실행 후 다음과 같은 에러가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;[kubelet-check] It seems like the kubelet isn't running or healthy.
[kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get http://localhost:10248/healthz: dial tcp 127.0.0.1:10248: connect: connection refused.
[kubelet-check] It seems like the kubelet isn't running or healthy.
[kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get http://localhost:10248/healthz: dial tcp 127.0.0.1:10248: connect: connection refused.
[kubelet-check] It seems like the kubelet isn't running or healthy.
[kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get http://localhost:10248/healthz: dial tcp 127.0.0.1:10248: connect: connection refused.
[kubelet-check] It seems like the kubelet isn't running or healthy.
[kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get http://localhost:10248/healthz: dial tcp 127.0.0.1:10248: connect: connection refused.
[kubelet-check] It seems like the kubelet isn't running or healthy.
[kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get http://localhost:10248/healthz: dial tcp 127.0.0.1:10248: connect: connection refused.
Unfortunately, an error has occurred:
            timed out waiting for the condition
This error is likely caused by:
            - The kubelet is not running
            - The kubelet is unhealthy due to a misconfiguration of the node in some way (required cgroups disabled)
            - No internet connection is available so the kubelet cannot pull or find the following control plane images:
                - k8s.gcr.io/kube-apiserver-amd64:v1.11.2
                - k8s.gcr.io/kube-controller-manager-amd64:v1.11.2
                - k8s.gcr.io/kube-scheduler-amd64:v1.11.2
                - k8s.gcr.io/etcd-amd64:3.2.18
                - You can check or miligate this in beforehand with &quot;kubeadm config images pull&quot; to make sure the images
                  are downloaded locally and cached.

        If you are on a systemd-powered system, you can try to troubleshoot the error with the following commands:
            - 'systemctl status kubelet'
            - 'journalctl -xeu kubelet'

        Additionally, a control plane component may have crashed or exited when started by the container runtime.
        To troubleshoot, list all containers using your preferred container runtimes CLI, e.g. docker.
        Here is one example how you may list all Kubernetes containers running in docker:
            - 'docker ps -a | grep kube | grep -v pause'
            Once you have found the failing container, you can inspect its logs with:
            - 'docker logs CONTAINERID'
couldn't initialize a Kubernetes cluster
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 다음과 같이 &lt;b&gt;/etc/docker/daemon.json&lt;/b&gt;을 추가해 준 후 systemctl을 이용하여 service를 재시작한다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
    &quot;exec-opts&quot;: [&quot;native.cgroupdriver=systemd&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;systemctl daemon-reload
systemctl restart docker
systemctl restart kubelet
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://stackoverflow.com/questions/52119985/kubeadm-init-shows-kubelet-isnt-running-or-healthy&quot;&gt;https://stackoverflow.com/questions/52119985/kubeadm-init-shows-kubelet-isnt-running-or-healthy&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;kubectl get node를 입력시 master 노드가 NotReady 상태로 보여지는것을 확인 할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kubectl은 &lt;b&gt;.kube/config&lt;/b&gt; 파일을 보고 실행된다. 이 파일이 없다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;The connection to the server localhost:8080 was refused - did you specify the right host or port?에러가 발생 할 수 있다. 해당 에러가 발생한다면 다음과 같은 조치를 취해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. worker노드로 사용할 vm에 접속하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;kubeadm init&lt;/b&gt;결과로 출력된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;kubeadm join &amp;lt;Kubernetes API Server:PORT&amp;gt; --token &amp;lt;Token 값&amp;gt; --discovery-token-ca-cert-hash sha256:&amp;lt;Hash 값&amp;gt;&lt;/b&gt;을 실행하여 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. master vm으로 돌아와&lt;span&gt;&amp;nbsp;&lt;/span&gt;kubectl get node를 입력하면 다음과 같은 결과를 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmnZ1b/btrn0XNyLaL/SQn57SpcvcsDqdXRZWNNqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmnZ1b/btrn0XNyLaL/SQn57SpcvcsDqdXRZWNNqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmnZ1b/btrn0XNyLaL/SQn57SpcvcsDqdXRZWNNqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmnZ1b%2Fbtrn0XNyLaL%2FSQn57SpcvcsDqdXRZWNNqk%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;591&quot; height=&quot;93&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-구성된-kubernetes-cluster에-간단한-nginx-pod-실행하기&quot; href=&quot;https://github.com/gksxodnd007/TIL/blob/master/k8s/virtualbox-cluster.md#%EA%B5%AC%EC%84%B1%EB%90%9C-kubernetes-cluster%EC%97%90-%EA%B0%84%EB%8B%A8%ED%95%9C-nginx-pod-%EC%8B%A4%ED%96%89%ED%95%98%EA%B8%B0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;&lt;b&gt;구성된 kubernetes cluster에 간단한 nginx pod 실행하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx-deployment.yaml&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx-service.yaml&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    app: nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 템플릿을 작성한 후 아래 명령어를 통해 deployment와 서비스를 배포한다.&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;kubectl apply -f ./nginx-deployment.yaml
kubectl apply -f ./nginx-service.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;kubectl get pods -o wide --sort-by=&quot;{.spec.nodeName}&quot;&lt;/b&gt;를 터미널에 입력하면 각 worker에 pod 배포된것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;95&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcH0Jz/btrn1y7A2BP/wwAL7DTLsoamzjaUOFhkd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcH0Jz/btrn1y7A2BP/wwAL7DTLsoamzjaUOFhkd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcH0Jz/btrn1y7A2BP/wwAL7DTLsoamzjaUOFhkd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcH0Jz%2Fbtrn1y7A2BP%2FwwAL7DTLsoamzjaUOFhkd1%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;1198&quot; height=&quot;95&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;95&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubectl describe svc를 터미널에 입력하면 접속 가능한 Endpoints들이 있는데 해당 Endpoints를 브라우저에 입력하면 nginx에서 응답해주는 화면도 볼 수 있을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dunlOE/btrn21IdzPK/lzSsftrhpks0SOYLTZdOY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dunlOE/btrn21IdzPK/lzSsftrhpks0SOYLTZdOY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dunlOE/btrn21IdzPK/lzSsftrhpks0SOYLTZdOY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdunlOE%2Fbtrn21IdzPK%2FlzSsftrhpks0SOYLTZdOY1%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;862&quot; height=&quot;311&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;311&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Server/Kubernetes</category>
      <author>코딩하는 오징어</author>
      <guid isPermaLink="true">https://effectivesquid.tistory.com/77</guid>
      <comments>https://effectivesquid.tistory.com/entry/VirtualBox%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-k8s-cluster-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0#entry77comment</comments>
      <pubDate>Wed, 15 Dec 2021 23:38:10 +0900</pubDate>
    </item>
    <item>
      <title>contact</title>
      <link>https://effectivesquid.tistory.com/notice/76</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;email: gksxodnd007@naver.com&lt;/p&gt;</description>
      <author>코딩하는 오징어</author>
      <guid isPermaLink="true">https://effectivesquid.tistory.com/notice/76</guid>
      <pubDate>Wed, 13 Oct 2021 12:20:34 +0900</pubDate>
    </item>
    <item>
      <title>Gradle Dependency Configuration</title>
      <link>https://effectivesquid.tistory.com/entry/Gradle-Dependency-Configuration</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;컴파일 언어를 이용하는 개발자들은 코드를 작성하고 해당 코드를 컴퓨터에서 실행시키기 위해 빌드라는 작업을 한다. 빌드라는 작업은 소스 코드를 컴파일 할 뿐만아니라 dependency를 추가해주고 실행가능한 bundle(묶음 파일)을 생성한다. java에서는 jar파일 또는 war파일이 될 수 있다. 이러한 결과물을 우리는 Artifact(인공물)라고 부른다. 개발자가 필요한 라이브러리와 소스 코드의 classpath를 직접 명시해주고 Artifact를 생성할 수 있지만, 대개의 경우 Maven이나 Gradle 같은 빌드 툴을 이용하여 해당 작업을 자동화한다. Maven은 pom.xml에 Gradle은 build.gradle 파일에 빌드 작업을 각 빌드 툴에서 제공하는 명세 혹은 DSL로 작성한다. 그러면 빌드 툴은 해당 파일을 통해 어떻게 빌드 해야할지 파악한 후 Artifact를 생성한다. 그 과정에서 어떤 시점에 필요한 라이브러리들의 classpath를 추가해줘야 하는지에 대한 고민을 이번 글에서 다뤄보려고한다. Maven은 scope라는 개념으로 Gradle에서는 dependency configuration이라는 개념으로 해당 기능을 제공한다. 이 글에서는 Gradle의 dependency configuration을 다뤄 보려고한다. 이 글을 읽고나면 Maven의 scope라는 개념도 쉽게 이해할 수 있다. &lt;b&gt;(컴파일만 잘 된다면 실행이 가능할 거라고 생각하면 큰 오산이다. runtime classpath와 compile classpath는 엄연히 다르다.)&lt;/b&gt;&lt;br /&gt;&amp;nbsp;먼저 Gradle에서 dependencies block을 사용하려면 다음과 같이 &lt;code&gt;java-library&lt;/code&gt; 혹은 &lt;code&gt;java&lt;/code&gt;플러그인을 추가해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;gt; &lt;i&gt;java-library는 이후에 설명할 api configuration을 사용할 수 있다.&lt;/i&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;plugins {
    id 'java-library' // or id 'java'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafka-clients, lombok, mysql-connector, h2database를 사용하여 프로젝트를 세팅한다고 가정해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kafka-client를 이용하여 이벤트를 kafka 발행할 것이다. compile &amp;amp; runtime 모두 필요하다.&lt;/li&gt;
&lt;li&gt;lombok은 compile시에만 사용된다.&lt;/li&gt;
&lt;li&gt;mysql-connector를 직접 사용하는 코드는 없기(컴파일을 할 필요 없음) 때문에 runtime시에만 사용된다.&lt;/li&gt;
&lt;li&gt;h2database는 테스트 runtime시에만 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 라이브러리들이 사용되는 시점을 잘 기억하길 바란다.&lt;/p&gt;
&lt;pre id=&quot;code_1632655345163&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.apache.kafka:kafka-clients:3.0.0'
    implementation 'org.projectlombok:lombok'
    implementation 'mysql:mysql-connector-java'
    testImplementation 'com.h2database:h2'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Gradle의 dependency configuration에 대한 개념이 부족하다면 모든 라이브러리들의 classpath 적용시점을 implementation으로 선언할 것이다. 물론 빌드는 잘 될 것이다. implemtation configuration은 runtime과 compile 모든 시점에 classpath를 추가해주기 때문이다. (gradle dependency configuration의 자세한 내용은 &lt;a href=&quot;https://docs.gradle.org/current/userguide/java_library_plugin.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gradle document&lt;/a&gt;를 참고하자.) 하지만 이렇게 설정한다면 특정 시점에&amp;nbsp; 특정 라이브러리가 필요하지 않음에도 불구하고 classpath를 추가하기 때문에 리소스 낭비이다. 위의&amp;nbsp; 라이브러리들이 사용되는 시점을 토대로 개선해보자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1632655968497&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.apache.kafka:kafka-clients:3.0.0'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'mysql:mysql-connector-java'
    testRuntimeOnly 'com.h2database:h2'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위와 같이 개선할 수 있다. kafka-clients를 runtimeOnly로 설정하게 되면 kafka-client를 사용한 KafkaProducer 클래스를 컴파일시점에 찾지 못해 compile이 실패할 것이고, compileOnly로만 설정하였다면 runtime시 해당 클래스를 참조할 classpath를 찾지 못하여 ClassNotFoundException와 NoClassDefFoundError가 발생할&amp;nbsp; 것이다. &lt;b&gt;(intellij로 실행할 경우 intellij가 classpath를 자동으로 추가하여 정상적으로 실행될 수 있다. 제대로 테스트를 해보려면 build 후 jar파일을 직접 실행하면 ClassNotFoundException와 NoClassDefFoundError가 발생할&amp;nbsp; 것이다.)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;지금 까지는 단순히 실행가능한 Artifact를 생성하는 관점에서 gradle dependency configuration을 살펴보았다. 이번에는 라이브러리로 제공하기 위해 생성되는 Artifact관점에서 살펴보자. 우리는 다음과 같은 상황을 가정해 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;934&quot; width=&quot;200&quot; height=&quot;343&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMHSFU/btrfU2DzAQ8/otxDuSrY3qmWYTIzx1dBmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMHSFU/btrfU2DzAQ8/otxDuSrY3qmWYTIzx1dBmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMHSFU/btrfU2DzAQ8/otxDuSrY3qmWYTIzx1dBmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMHSFU%2FbtrfU2DzAQ8%2FotxDuSrY3qmWYTIzx1dBmk%2Fimg.png&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;934&quot; width=&quot;200&quot; height=&quot;343&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통적으로 사용할 common이라는 library가 있고 storage 접근을 위한 repository library가 있다. repository library는 common library에대한 의존성 추가하여 개발되었고 api project는 repository library에대한 의존성을 추가 하여 비즈니스 로직을 개발하고 있다고 가정해보자. 우리는 repository 프로젝트의 build.gradle을 다음과 같이 작성하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1632660445071&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'java-library'
}

dependencies {
    implementation 'org.codingsuqid:common:1.0.0'
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 dependency configuration을 작성한 후 api 프로젝트의 build.gradle을 다음과 같이 작성하면 api 프로젝트에서는 common library를 사용할 수 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1632660566638&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'java-library'
}

dependencies {
    implementation 'org.codingsuqid:repository:1.0.0'
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다음과 같이 repository의 dependency configuration을 implementation대신 api(java-library plugin에서만 사용할 수 있다.)를 이용한다면 api 프로젝트에서도 common을 사용할 수 있게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1632660721286&quot; class=&quot;javascript&quot; style=&quot;display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'java-library'
}

dependencies {
    api 'org.codingsuqid:common:1.0.0'
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, implementation과 api의 차이는 transitive 정도의 차이이다. api로 dependency를 추가하게되면 repository를 의존하고 있는 다른 프로젝트에서도 common에 대한 api들이 노출되어 사용할 수 있게 되는 것이다. 따라서 api는 신중하게 사용하자.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;343&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCH5mP/btrf1idQjjy/4i6HrJxYdZ0YkSKVKQlC3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCH5mP/btrf1idQjjy/4i6HrJxYdZ0YkSKVKQlC3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCH5mP/btrf1idQjjy/4i6HrJxYdZ0YkSKVKQlC3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCH5mP%2Fbtrf1idQjjy%2F4i6HrJxYdZ0YkSKVKQlC3k%2Fimg.png&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;343&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 그림을 보면 common이 수정되면 implementation configuration같은 경우에는 repository만 rebuild하면 되지만 api configuration같은 경우에는 api 프로젝트도 rebuild되어야 한다. 따라서 정말 필요한 상황이 아니라면 implementation로 configuration을 설정하는 것이 빌드 속도가 더 향상될 수 있다. &lt;b&gt;gradle 6.x까지 지원되던 compile configuration은 api와 동일하며 7.x부터 사용할 수 없기 때문에 더 이상 사용하지 말자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고:&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://bluayer.com/13&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://bluayer.com/13&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1632661168602&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;[Gradle] implementation vs compile&quot; data-og-description=&quot;서론 Gradle dependency 관련해서 검색을 하다보면, 어떤 글에서는 implementation을 사용하고 어떤 글에서는 compile을 사용하는 경우가 있다. 사실 어떻게 사용해도 돌아가긴 해서, 음... 무슨 차이지?하고&quot; data-og-host=&quot;bluayer.com&quot; data-og-source-url=&quot;https://bluayer.com/13&quot; data-og-url=&quot;https://bluayer.com/13&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/q7FSS/hyLKdSM8iF/LWZFrfMTlUE62eBgNIkd61/img.png?width=800&amp;amp;height=481&amp;amp;face=0_0_800_481,https://scrap.kakaocdn.net/dn/2JQo5/hyLKgvcARp/MjqpfPsKdAS6GJnZTczYYk/img.png?width=800&amp;amp;height=481&amp;amp;face=0_0_800_481,https://scrap.kakaocdn.net/dn/cMuXQU/hyLKngLIOU/WPP5jVr1ZLZvm1YqRxCXh1/img.png?width=806&amp;amp;height=485&amp;amp;face=0_0_806_485&quot;&gt;&lt;a href=&quot;https://bluayer.com/13&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bluayer.com/13&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/q7FSS/hyLKdSM8iF/LWZFrfMTlUE62eBgNIkd61/img.png?width=800&amp;amp;height=481&amp;amp;face=0_0_800_481,https://scrap.kakaocdn.net/dn/2JQo5/hyLKgvcARp/MjqpfPsKdAS6GJnZTczYYk/img.png?width=800&amp;amp;height=481&amp;amp;face=0_0_800_481,https://scrap.kakaocdn.net/dn/cMuXQU/hyLKngLIOU/WPP5jVr1ZLZvm1YqRxCXh1/img.png?width=806&amp;amp;height=485&amp;amp;face=0_0_806_485');&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;[Gradle] implementation vs compile&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론 Gradle dependency 관련해서 검색을 하다보면, 어떤 글에서는 implementation을 사용하고 어떤 글에서는 compile을 사용하는 경우가 있다. 사실 어떻게 사용해도 돌아가긴 해서, 음... 무슨 차이지?하고&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bluayer.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://olivejua-develop.tistory.com/59&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://olivejua-develop.tistory.com/59&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1632661291956&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;Maven Scopes? Gradle Configurations?&quot; data-og-description=&quot;대부분의 내용은 reflectoring.io/maven-scopes-gradle-configurations/ 개인적으로 의미를 해석한 것입니다. 빌드 툴은 의존성(dependency)을 관리한다. 우리가 어떠한 라이브러리를 프로젝트에서 사용하고 싶을.&quot; data-og-host=&quot;olivejua-develop.tistory.com&quot; data-og-source-url=&quot;https://olivejua-develop.tistory.com/59&quot; data-og-url=&quot;https://olivejua-develop.tistory.com/59&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/6RAPi/hyLKl4mqn6/xqSfkZn2eVWypryYl7CFX0/img.png?width=788&amp;amp;height=247&amp;amp;face=0_0_788_247,https://scrap.kakaocdn.net/dn/YfH1X/hyLKg22m65/UOvUs4Ren82x9ZdktvNQc0/img.png?width=788&amp;amp;height=247&amp;amp;face=0_0_788_247,https://scrap.kakaocdn.net/dn/dDalgB/hyLKeKTZot/aZlqRQwXCm5Zonl1h5SpT1/img.png?width=920&amp;amp;height=267&amp;amp;face=0_0_920_267&quot;&gt;&lt;a href=&quot;https://olivejua-develop.tistory.com/59&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://olivejua-develop.tistory.com/59&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/6RAPi/hyLKl4mqn6/xqSfkZn2eVWypryYl7CFX0/img.png?width=788&amp;amp;height=247&amp;amp;face=0_0_788_247,https://scrap.kakaocdn.net/dn/YfH1X/hyLKg22m65/UOvUs4Ren82x9ZdktvNQc0/img.png?width=788&amp;amp;height=247&amp;amp;face=0_0_788_247,https://scrap.kakaocdn.net/dn/dDalgB/hyLKeKTZot/aZlqRQwXCm5Zonl1h5SpT1/img.png?width=920&amp;amp;height=267&amp;amp;face=0_0_920_267');&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;Maven Scopes? Gradle Configurations?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 내용은 reflectoring.io/maven-scopes-gradle-configurations/ 개인적으로 의미를 해석한 것입니다. 빌드 툴은 의존성(dependency)을 관리한다. 우리가 어떠한 라이브러리를 프로젝트에서 사용하고 싶을.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;olivejua-develop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gradle.org/current/userguide/java_library_plugin.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.gradle.org/current/userguide/java_library_plugin.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1632661339094&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;The Java Library Plugin&quot; data-og-description=&quot;The Java Library plugin expands the capabilities of the Java plugin by providing specific knowledge about Java libraries. In particular, a Java library exposes an API to consumers (i.e., other projects using the Java or the Java Library plugin). All the so&quot; data-og-host=&quot;docs.gradle.org&quot; data-og-source-url=&quot;https://docs.gradle.org/current/userguide/java_library_plugin.html&quot; data-og-url=&quot;https://docs.gradle.org/current/userguide/java_library_plugin.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.gradle.org/current/userguide/java_library_plugin.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.gradle.org/current/userguide/java_library_plugin.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;The Java Library Plugin&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Java Library plugin expands the capabilities of the Java plugin by providing specific knowledge about Java libraries. In particular, a Java library exposes an API to consumers (i.e., other projects using the Java or the Java Library plugin). All the so&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.gradle.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알쓸신잡</category>
      <author>코딩하는 오징어</author>
      <guid isPermaLink="true">https://effectivesquid.tistory.com/75</guid>
      <comments>https://effectivesquid.tistory.com/entry/Gradle-Dependency-Configuration#entry75comment</comments>
      <pubDate>Sun, 26 Sep 2021 22:02:39 +0900</pubDate>
    </item>
    <item>
      <title>TransactionSynchronizationManager를 이용하여 DataSource 라우팅시 주의할 점</title>
      <link>https://effectivesquid.tistory.com/entry/TransactionSynchronizationManager%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-DataSource-%EB%9D%BC%EC%9A%B0%ED%8C%85%EC%8B%9C-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;spring web mvc를 이용하여 서버 어플리케이션을 개발한다면 @Transactional을 이용하여 트랜잭션을 적용할 것이다. @Transactional이 적용된 메서드는 다음과 같은 flow로 메서드가 실행된다. (PlatformTransactionManager, DataSource 인터페이스에 대한 자세한 설명은 이 글에서 다루지 않는다.)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;1. CglibAopProxy.DynamicAdvisedInterceptor.intercept(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;2. TransactionInterceptor.invoke(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;3. TransactionAspectSupport.invokeWithinTransaction(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;4. TransactionAspectSupport.createTransactionIfNecessary(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;5. AbstractPlatformTransactionManager.getTransaction(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;6. AbstractPlatformTransactionManager.startTransaction(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;7. AbstractPlatformTransactionManager.begin(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;8. AbstractPlatformTransactionManager.prepareSynchronization(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;...&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;9. (4번 ~ 8번 ~ ...) 과정을 마치고 돌아와서 TransactionAspectSupport.invokeWithinTransaction(...)이 계속 진행되며 이후 파라미터로 받은 InvocationCallback의 proceedWithInvocation(...)을 호출&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;10. @Transactional이 적용된 실제 메서드가 실행&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;...&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 글에서는 위의 flow를 한번 더 참조할 예정이므로 주의깊게 살펴보자. 해당 코드의 실행환경은 spring boot 2.4.1 이며 spring boot 2.x에서는 cglib proxy가 기본 설정이므로 aop proxy관련해서 별도로 설정한 것이 없다면 cglib proxy를 사용하게 된다. 1번 과정에서 트랜잭션 aop proxy로 cglib proxy를 사용하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;본 내용에 들어가기위한 준비는 끝났다. 이제 본론으로 들어가보자. RDBMS를 사용하는 보통의 운영환경에서는 고가용성을 위해 replication db를 구성하게되며 이를 slave db라고 부른다. 주로 master db가 서버 어플리케이션의 읽기와 쓰기를 처리하게되고 master db에 장애가 발생했을 경우 slave db를 master db로 승격시켜 장애 시간을 최소화한다. 이외에도 batch application이나 분석을 위해 etl대상으로 slave db로 읽기 요청을 보내고 싶은 경우가 있다. 이러한 경우를 위해 필자는 &lt;span style=&quot;color: #f3c000; background-color: #000000;&quot;&gt;@Transactional(readOnly = true)&lt;/span&gt;를 선언한 메서드는 slave db로 읽기 요청을 보내도록 하고 싶었다. 여러가지 방법이 있겠지만 &quot;&lt;b&gt;&lt;i&gt;spring boot routing readonly datasource&quot;&lt;/i&gt;&lt;/b&gt;로 검색해보면 가장 많이 보이고 추천하는 방식으로&amp;nbsp;&lt;b&gt;AbstractRoutingDataSource&lt;/b&gt;의 determineCurrentLookupKey() 메서드를 구현하는 DataSource를 Bean으로 등록하는 방법을 소개한다. &lt;b&gt;AbstractRoutingDataSource&lt;/b&gt;클래스를 살펴보면 해당 메서드가 어디에 사용되는지 확인할 수 있다. 코드를 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1631353125385&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    @Nullable
    private Map&amp;lt;Object, Object&amp;gt; targetDataSources;

    @Nullable
    private Object defaultTargetDataSource;

    private boolean lenientFallback = true;

    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

    @Nullable
    private Map&amp;lt;Object, DataSource&amp;gt; resolvedDataSources;

    @Nullable
    private DataSource resolvedDefaultDataSource;
    /* 생략... */

    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }
    /* 생략... */

    /**
     * Retrieve the current target DataSource. Determines the
     * {@link #determineCurrentLookupKey() current lookup key}, performs
     * a lookup in the {@link #setTargetDataSources targetDataSources} map,
     * falls back to the specified
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
     * @see #determineCurrentLookupKey()
     */
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, &quot;DataSource router not initialized&quot;);
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null &amp;amp;&amp;amp; (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException(&quot;Cannot determine target DataSource for lookup key [&quot; + lookupKey + &quot;]&quot;);
        }
        return dataSource;
    }

    /**
     * Determine the current lookup key. This will typically be
     * implemented to check a thread-bound transaction context.
     * &amp;lt;p&amp;gt;Allows for arbitrary keys. The returned key needs
     * to match the stored lookup key type, as resolved by the
     * {@link #resolveSpecifiedLookupKey} method.
     */
    @Nullable
    protected abstract Object determineCurrentLookupKey();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AbstractRoutingDataSource에는 여러 메서드들이 있지만 필요한 부분만 옮겨왔다. 우리가 override해야하는 determineCurrentLookupKey()는 DataSource로 부터 Connection을 가져올때 어떤 DataSource로 부터 Connection을 가져올건지 정하기 위한 로직을 구현해야한다. spring web mvc를 이용한다면 트랜잭션은 쓰레드 별로 유지되며 이를 위해 ThreadLocal&amp;lt;T&amp;gt;을 이용하여 트랜잭션 정보를 저장한다. 이러한 정보는 TransactionSynchronizationManager가 동기화 및 관리하게된다. 이러한 지식들을 바탕으로 필자는 다음과 같은 DatabaseConfig를 설정 코드로 작성하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1631355064171&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@EnableConfigurationProperties(value = {MasterDatabaseProperties.class, SlaveDatabaseProperties.class, PlayJpaProperties.class})
public class DatabaseConfig {

    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }

    @Bean
    public DataSource dataSource(MasterDatabaseProperties masterDatabaseProperties, SlaveDatabaseProperties slaveDatabaseProperties) {
        DataSource master = createDataSource(masterDatabaseProperties);
        DataSource slave = createDataSource(slaveDatabaseProperties);
        Map&amp;lt;Object, Object&amp;gt; dataSourceMap = new HashMap&amp;lt;&amp;gt;();
        dataSourceMap.put(RoutingDataSource.RoutingKey.MASTER, master);
        dataSourceMap.put(RoutingDataSource.RoutingKey.SLAVE, slave);

        AbstractRoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setDefaultTargetDataSource(master);
        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.afterPropertiesSet();

        return routingDataSource;
    }

    public static DataSource createDataSource(DatabaseProperties properties) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(properties.getUrl());
        dataSource.setUsername(properties.getUsername());
        dataSource.setPassword(properties.getPassword());
        dataSource.setMaximumPoolSize(properties.getMaxConnection());
        dataSource.setMinimumIdle(properties.getMinConnection());
        return dataSource;
    }

    public static class RoutingDataSource extends AbstractRoutingDataSource {

        enum RoutingKey {
            MASTER, SLAVE
        }

        @Override
        protected Object determineCurrentLookupKey() {
            // 현재 트랜잭션이 readOnly로 설정되어있는지 확인
            if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
                log.info(&quot;determine slave db...&quot;);
                return RoutingKey.SLAVE;
            }

            log.info(&quot;determine master db...&quot;);
            return RoutingKey.MASTER;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 트랜잭션이 readOnly 설정으로 되어있는 트랜잭션인지 확인 후 적절한 routing key를 return하는 로직을 구현한 RoutingDataSource를 DataSource Bean으로 등록하였다. DataSource에서 Connection을 올바르게 가져오는지 확인하기 위해 &lt;span style=&quot;background-color: #000000; color: #f3c000;&quot;&gt;@Transactional(readOnly = true)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 적용한 메서드를 실행시켜 보았다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;86&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pU53y/btreLKipOKM/9KM3tGNOrqTXoxKOr7NEr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pU53y/btreLKipOKM/9KM3tGNOrqTXoxKOr7NEr1/img.png&quot; data-alt=&quot;로그-1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pU53y/btreLKipOKM/9KM3tGNOrqTXoxKOr7NEr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpU53y%2FbtreLKipOKM%2F9KM3tGNOrqTXoxKOr7NEr1%2Fimg.png&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;86&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로그-1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;determine slave db...&quot;가 로그로 출력 되기를 기대했지만 &quot;determine master db...&quot;가 출력되었다. 이게 무슨 일인가 싶어 디버깅을 해봤더니 다음과 같은 상황을 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2770&quot; data-origin-height=&quot;1190&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m6CWv/btreJQRn4bJ/FgX2kzK6wzE8TB9p72kpjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m6CWv/btreJQRn4bJ/FgX2kzK6wzE8TB9p72kpjK/img.png&quot; data-alt=&quot;디버깅-1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m6CWv/btreJQRn4bJ/FgX2kzK6wzE8TB9p72kpjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm6CWv%2FbtreJQRn4bJ%2FFgX2kzK6wzE8TB9p72kpjK%2Fimg.png&quot; data-origin-width=&quot;2770&quot; data-origin-height=&quot;1190&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;디버깅-1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #f3c000;&quot;&gt;@Transactional(readOnly = true)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 적용한 메서드를 실행시켰음에도 불구하고 현재 트랜잭션은 readOnly상태가 아니었다. 글을 시작하면서 설명한 spring에서 트랜잭션 처리가 이루어지는 과정을 다시 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;1. CglibAopProxy.DynamicAdvisedInterceptor.intercept(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;2. TransactionInterceptor.invoke(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;3. TransactionAspectSupport.invokeWithinTransaction(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;4. TransactionAspectSupport.createTransactionIfNecessary(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;5. AbstractPlatformTransactionManager.getTransaction(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;6. AbstractPlatformTransactionManager.startTransaction(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;7. AbstractPlatformTransactionManager.begin(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;8. AbstractPlatformTransactionManager.prepareSynchronization(...)&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;...&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;9. (4번 ~ 8번 ~ ...) 과정을 마치고 돌아와서 TransactionAspectSupport.invokeWithinTransaction(...)이 계속 진행되며 이후 파라미터로 받은 InvocationCallback의 proceedWithInvocation(...)을 호출&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;10. @Transactional이 적용된 실제 메서드가 실행&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;i&gt;&lt;b&gt;...&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7번 과정과 8번 과정을 자세히 살펴보면 (테스트 코드는 Persistence Framework로 JPA를 이용하였으므로 JpaTransactionManager의 코드를 살펴보자) 7번 과정에서 DataSource로 부터 Connection을 가져오고 8번 과정에서 트랜잭션의 현재 상태를 ThreadLocal&amp;lt;T&amp;gt;에 저장해둔다. 즉, TransactionSynchronizationManager에 트랜잭션의 정보를 동기화 하는 작업이 DataSource로 부터 Connection을 가져온 (Routing 로직이 실행) 이후에 실행된다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;574&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GSKfq/btreOsBuvUs/EDjkCiKKPqA91ojAwXfOuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GSKfq/btreOsBuvUs/EDjkCiKKPqA91ojAwXfOuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GSKfq/btreOsBuvUs/EDjkCiKKPqA91ojAwXfOuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGSKfq%2FbtreOsBuvUs%2FEDjkCiKKPqA91ojAwXfOuK%2Fimg.png&quot; data-origin-width=&quot;1764&quot; data-origin-height=&quot;574&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 8번 과정의 코드이다. readOnly 정보가 동기화되는 것을 확인할 수 있다. Routing 로직이 실행 된 이후에 readOnly 값이 TransactionSynchronizationManager에 동기화 되므로 디버깅 했을 때 확인했던 TransactionSynchronizationManager.isCurrentTrasactionReadOnly() 결과로 true가 아닌 boolean 타입의 초기화 default 값인 false가 return된 것이다. 그에 따라 slave db로의 Connection Pool을 갖고 있는 DataSource를 갖고오지 못하게 되는 것이다. spring에서 트랜잭션이 시작되면 트랜잭션이 종료될 때 까지 같은 Connection을 이용하게 되므로 메서드가 종료될 때 까지 master db를 통해 데이터를 조회하게 된다. 이러한 문제를 해결하려면 여러 방법이 있겠지만 실제로 Connection이 필요할 때 Connection을 가져오는 로직을 실행시키면 많은 코드 수정 없이 문제를 해결할 수 있다. 바로 이런 역할을 하는 DataSource가 LazyConnectionDataSourceProxy이다. LazyConnectionDataSourceProxy는 실제 Connection을 통해 데이터 조회를 할 때 Connection을 가져오게된다. 실제로 LazyConnectionDataSourceProxy의 getConnection() 메서드를 살펴보면 Jdk Proxy를 이용하여 Connection을 return한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;600&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br9nRU/btreLfv8wQF/yxUl2nDr6zN0PKPA4pFWo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br9nRU/btreLfv8wQF/yxUl2nDr6zN0PKPA4pFWo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br9nRU/btreLfv8wQF/yxUl2nDr6zN0PKPA4pFWo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr9nRU%2FbtreLfv8wQF%2FyxUl2nDr6zN0PKPA4pFWo1%2Fimg.png&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;600&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석을 보면 statement를 실행할때 actual JDBC Connection을 fetch한다고 설명되어있다. 이제 방법을 알았으니 다음과 같이 DataSource를 설정하고 실행시켜보자.&lt;/p&gt;
&lt;pre id=&quot;code_1631358288916&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@EnableConfigurationProperties(value = {MasterDatabaseProperties.class, SlaveDatabaseProperties.class, PlayJpaProperties.class})
public class DatabaseConfig {

    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }

    @Bean
    public DataSource dataSource(MasterDatabaseProperties masterDatabaseProperties, SlaveDatabaseProperties slaveDatabaseProperties) {
        DataSource master = createDataSource(masterDatabaseProperties);
        DataSource slave = createDataSource(slaveDatabaseProperties);
        Map&amp;lt;Object, Object&amp;gt; dataSourceMap = new HashMap&amp;lt;&amp;gt;();
        dataSourceMap.put(RoutingDataSource.RoutingKey.MASTER, master);
        dataSourceMap.put(RoutingDataSource.RoutingKey.SLAVE, slave);

        AbstractRoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setDefaultTargetDataSource(master);
        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.afterPropertiesSet();

        // RoutingDataSource를 그대로 bean으로 사용하지 않고
        // LazyConnectionDataSourceProxy로 한번 감싸준 후에 bean으로 사용한다.
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }

    public static DataSource createDataSource(DatabaseProperties properties) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(properties.getUrl());
        dataSource.setUsername(properties.getUsername());
        dataSource.setPassword(properties.getPassword());
        dataSource.setMaximumPoolSize(properties.getMaxConnection());
        dataSource.setMinimumIdle(properties.getMinConnection());
        return dataSource;
    }

    public static class RoutingDataSource extends AbstractRoutingDataSource {

        enum RoutingKey {
            MASTER, SLAVE
        }

        @Override
        protected Object determineCurrentLookupKey() {
            // 현재 트랜잭션이 readOnly로 설정되어있는지 확인
            if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
                log.info(&quot;determine slave db...&quot;);
                return RoutingKey.SLAVE;
            }

            log.info(&quot;determine master db...&quot;);
            return RoutingKey.MASTER;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RoutingDataSource를 LazyConnectionDataSourceProxy로 한번 감싸준 후에 bean으로 사용한다. 실행 결과는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2124&quot; data-origin-height=&quot;176&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xLHHi/btreLDQWCuI/V5UGCQZK4uVwXJmf332PN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xLHHi/btreLDQWCuI/V5UGCQZK4uVwXJmf332PN1/img.png&quot; data-alt=&quot;로그-2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xLHHi/btreLDQWCuI/V5UGCQZK4uVwXJmf332PN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxLHHi%2FbtreLDQWCuI%2FV5UGCQZK4uVwXJmf332PN1%2Fimg.png&quot; data-origin-width=&quot;2124&quot; data-origin-height=&quot;176&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로그-2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;slave db와 연결된 DataSource가 잘 선택된 것을 확인할 수 있다. 그리고 이전과 다르게 실제 statement(query 문)가 전달 될 때 Connection을 가져오게 된다. (로그 순서를 보면 로그-1과는 다른 것을 알 수 있다.)&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;86&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvVg2c/btreLgIA5CU/qnAUkzgZt1FuYv4X3wwXyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvVg2c/btreLgIA5CU/qnAUkzgZt1FuYv4X3wwXyk/img.png&quot; data-alt=&quot;로그-1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvVg2c/btreLgIA5CU/qnAUkzgZt1FuYv4X3wwXyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvVg2c%2FbtreLgIA5CU%2FqnAUkzgZt1FuYv4X3wwXyk%2Fimg.png&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;86&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로그-1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 주제를 다루는 많은 글들이 있었지만 실제 코드를 직접 보면서 해당 문제의 원인을 살펴보는 글을 작성해보고 싶었다. 이 글이 보다 더 도움이 되는 글이 되기를 바라며 글을 마치겠다.&lt;/p&gt;</description>
      <category>Framework/Spring</category>
      <author>코딩하는 오징어</author>
      <guid isPermaLink="true">https://effectivesquid.tistory.com/74</guid>
      <comments>https://effectivesquid.tistory.com/entry/TransactionSynchronizationManager%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-DataSource-%EB%9D%BC%EC%9A%B0%ED%8C%85%EC%8B%9C-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90#entry74comment</comments>
      <pubDate>Fri, 10 Sep 2021 22:35:29 +0900</pubDate>
    </item>
    <item>
      <title>JVM의 종료와 Graceful Shutdown</title>
      <link>https://effectivesquid.tistory.com/entry/JVM%EC%9D%98-%EC%A2%85%EB%A3%8C%EC%99%80-Graceful-Shutdown</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;개발자는 어플리케이션을 개발 할 때 많은 것들을 고려한다. 코드를 작성하고 나서는 비즈니스 로직이 정확한 결과를 산출해내는지 검증하기 위해 테스트 코드를 작성하기도 하며, 성능 테스트를 통해 시스템에 병목 지점은 없는지 등을 확인한다. 어플리케이션이 정상적으로 시작되고 실행이 지속되는지는 매우 중요하다. 하지만 어플리케이션이 정상적으로 종료되는지도 굉장히 중요하다. 이번 글을 통해서 JVM 플랫폼 위에서 실행되는 어플리케이션이 정상적으로 종료되기 위한 여러 내용들을 소개하려고한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프로세스 종료&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;먼저, 프로세스를 종료시키기 위해 프로세스로 전달하는 시그널에 대해서 살펴보자. 시그널이 전달되면 시스템은 다음과 같이 동작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;시그널에대한 핸들러는 커널에 프로그래밍 되어 있으며 프로세스가 시그널을 받게 되면 시그널에 해당하는 bit를 마킹한다.&lt;/li&gt;
&lt;li&gt;다음 명령어가 진행될 때 마킹된 bit를 확인 후 커널에게 제어를 넘긴다. (Context Switching 발생)&lt;/li&gt;
&lt;li&gt;벡터를 통해 적절한 시그널 핸들러를 찾아 핸들러를 실행시킨다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스를 종료 시키는 다음과 같은 시그널들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SIGKILL&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SIGTERM&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SIGINT&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;SIGQUIT&lt;/li&gt;
&lt;li&gt;SIGSTP&lt;/li&gt;
&lt;li&gt;SIGHUP&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 SIGKILL, SIGTERM, SIGINT에 대해서만 알아보겠다. 다른 시그널에 대한 내용도 살펴보고 싶다면 다음 링크를 참조하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://programmergamer.blogspot.com/2013/05/clarification-on-sigint-sigterm-sigkill.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://programmergamer.blogspot.com/2013/05/clarification-on-sigint-sigterm-sigkill.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1621398866243&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;Clarification on SIGKILL, SIGTERM, SIGINT, SIGQUIT, SIGSTP and SIGHUP&quot; data-og-description=&quot;A few days ago, i landed upon unix signals that lead to process termination. I guess i was trying to remember the signals generated in linu...&quot; data-og-host=&quot;programmergamer.blogspot.com&quot; data-og-source-url=&quot;http://programmergamer.blogspot.com/2013/05/clarification-on-sigint-sigterm-sigkill.html&quot; data-og-url=&quot;http://programmergamer.blogspot.com/2013/05/clarification-on-sigint-sigterm-sigkill.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;http://programmergamer.blogspot.com/2013/05/clarification-on-sigint-sigterm-sigkill.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;http://programmergamer.blogspot.com/2013/05/clarification-on-sigint-sigterm-sigkill.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Clarification on SIGKILL, SIGTERM, SIGINT, SIGQUIT, SIGSTP and SIGHUP&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A few days ago, i landed upon unix signals that lead to process termination. I guess i was trying to remember the signals generated in linu...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmergamer.blogspot.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SIGKILL&lt;/b&gt;은 프로세스를 즉시 종료 시킨다. 해당 시그널을 받으면 프로세스가 종료되기 전에 수행되어야 하는 종료 절차를 실행하지 않고 즉시 종료하므로 graceful shutdown을 위해서라면 해당 시그널을 통해 프로세스를 종료시키는 것은 피해야한다. kill 명령어의 옵션 인자로 -9를 주면 프로세스에 &lt;b&gt;SIGKILL&lt;/b&gt; 시그널을 전달하게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;gt; kill -9 {PID}&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SIGTERM &lt;/b&gt;시그널도 프로세스를 종료시키지만 프로세스를 종료 시키기전에 해당 시그널을 핸들링 할 수 있다. 즉, 프로세스를 종료시키기 전에 종료 절차를 진행한 후에 프로세스를 종료 시킨다. 만약 프로세스가 해당 시그널을 핸들링하는 코드를 작성하지 않았다면 즉시 종료시킨다. graceful shutdown을 위해서라면 해당 시그널을 사용하면 된다. kill 명령어에 어떠한 옵션인자도 주어지지 않는다면 &lt;b&gt;SIGTERM&lt;/b&gt;을 프로세스로 전달하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;gt; kill {PID}&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SIGINT &lt;/b&gt;시그널은&lt;b&gt; SIGTERM&lt;/b&gt;과 동일하지만 시그널을 보내기 위한 트리거를 키보드로 부터 받는다. CTRL + C를 입력하여 프로세스를 종료시킬때 &lt;b&gt;SIGINT&lt;/b&gt;가 전달된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JVM 종료&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM은 다음과 같은 경우에 정상적인 종료 절차를 밟게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데몬 스레드가 아닌 일반 스레드가 모두 종료되는 시점&lt;/li&gt;
&lt;li&gt;System.exit 메서드가 호출 될 경우&lt;/li&gt;
&lt;li&gt;프로세스가 종료 시그널을 받게 된 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위와 같은 상황을 통해 JVM이 종료된다면 JVM은 가장 먼저 등록되어 있는 모든 shutdown-hook을 실행시킨다. shutdown-hook은 Runtime 클래스의 addShutdownHook(Thread hook) 메서드를 통해 등록 할 수 있다. 하나의 JVM에 여러 개의 shutdown-hook을 등록 할 수 있다. 다만, 두 개 이상의 shutdown-hook이 등록되어 있는 경우에는 random으로 실행된다. 등록된 shutdown-hook들은&amp;nbsp; 종료 시점에 동시에 실행되므로 thread-safe하게 코드를 작성해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;JVM에서 종료 절차가 시작됐는데 어플리케이션에서 사용하던 스레드가 계속해서 동작 중이라면 종료 절차가 진행되는 과정 내내 기존의 스레드도 계속해서 실행되기도 한다. JVM은 종료 과정에서 계속해서 실행되고 있는 어플리케이션 내부의 스레드에 대해 중단 절차를 진행하거나 인터럽트를 걸지 않는다. 계속해서 실행되던 스레드는 결국 종료 절차가 끝나는 시점에 강제로 종료된다. 만약 shutdown-hook이나 finalize 메서드가 작업을 마치지 못하고 계속해서 실행된다면 종료 절차가 멈추는 셈이며 JVM은 계속해서 대기 상태로 머무르기 때문에 결국 JVM을 강제로 종료하는 수밖에 없다. JVM을 강제로 종료시킬 때는 JVM이 스스로 종료되는 것 이외에 shutdown-hook을 실행하는 등의 어떤 작업도 하지 않는다. 따라서 위에 말했다시피 shutdown-hook은 dead lock이나 hang이 걸리지 않도록 안전하게 개발해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Spring Context는 종료 시점에 사용하던 bean들을 정리하는 등의 Context를 정리하는 코드를 shutdown-hook으로 추가한다. 다음 코드는 Spring에서 Runtime.addShutdownHook(Thread hook) 메서드를 사용하는 것을 보여준다.&lt;/p&gt;
&lt;pre id=&quot;code_1621436010472&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 20px; color: #383a42; background: #f8f8f8; font-size: 14px; font-family: 'SF Mono', Menlo, Consolas, Monaco, monospace; border: 1px solid #ebebeb; line-height: 1.71; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext {
    ...

    @Override
    public void registerShutdownHook() {
        if (this.shutdownHook == null) {
            // No shutdown hook registered yet.
            this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
                @Override
                public void run() {
                    synchronized (startupShutdownMonitor) {
                        doClose();
                    }
                }
            };
            Runtime.getRuntime().addShutdownHook(this.shutdownHook);
        }
    }
    ...
    
    protected void doClose() {
        // Check whether an actual close attempt is necessary...
        if (this.active.get() &amp;amp;&amp;amp; this.closed.compareAndSet(false, true)) {
            if (logger.isDebugEnabled()) {
                logger.debug(&quot;Closing &quot; + this);
            }

            if (!NativeDetector.inNativeImage()) {
                LiveBeansView.unregisterApplicationContext(this);
            }

            try {
                // Publish shutdown event.
                publishEvent(new ContextClosedEvent(this));
			}
            catch (Throwable ex) {
                logger.warn(&quot;Exception thrown from ApplicationListener handling ContextClosedEvent&quot;, ex);
			}

            // Stop all Lifecycle beans, to avoid delays during individual destruction.
            if (this.lifecycleProcessor != null) {
                try {
                    this.lifecycleProcessor.onClose();
                }
                catch (Throwable ex) {
                    logger.warn(&quot;Exception thrown from LifecycleProcessor on context close&quot;, ex);
                }
            }

            // Destroy all cached singletons in the context's BeanFactory.
            destroyBeans();

            // Close the state of this context itself.
            closeBeanFactory();

            // Let subclasses do some final clean-up if they wish...
            onClose();

            // Reset local application listeners to pre-refresh state.
            if (this.earlyApplicationListeners != null) {
                this.applicationListeners.clear();
                this.applicationListeners.addAll(this.earlyApplicationListeners);
            }

            // Switch to inactive.
            this.active.set(false);
        }
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Graceful Shutdown&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;어플리케이션을 개발할 때 반드시 고려해야할 요소들이 몇 가지 있다. 사용할 리소스들을 초기화 화거나 리소스를 초기화하는데 오랜 시간이 걸린다면 Lazy Loading을 통해 어플리케이션이 구동되는데 걸리는 시간을 줄이기도 한다. 어떤 로그를 남길 것이고 로그를 남기는데 필요한 도구들도 고려해야한다. 무중단 서비스를 제공하면서 동시에 성능을 수평적으로 높이기 위한 scale out을 고려하여 어플리케이션을 stateless하게 개발하기도한다. 이외에도 많은 것들을 고려하지만 어플리케이션을 잘 종료하는 것도 고려해야한다. 개발자는 종종 어플리케이션의 종료에 대해서는 소홀하게 생각하게된다. 어플리케이션이 사용하던 자원들을 반납하고 현재 처리 중이던 task들을 정리해야한다. 그렇지 않으면 어플리케이션을 종료 해야하는 상황마다 버그가 발생할 수 있다.&amp;nbsp; 어플리케이션의 타입이 Web Server Application이라면 어플리케이션이 종료 절차를 밟은 뒤 부터는 더 이상 요청을 받지 않아야 하며, 이미 받은 요청이 존재한다면 해당 요청들을 모두 처리한 후에 어플리케이션을 종료하여야한다. 그렇지 않으면 이미 요청을 한 클라이언트는 4-way hand shake 절차가 생략되어 Connection Reset 응답을 받음으로써 에러 응답을 받는 등의 문제가 발생 할 수 있다. 어플리케이션의 타입이 Batch Application이라면 처리중이던 Task를 모두 처리하고 종료하거나 현재 까지 처리된 지점에 save point를 만들어 다시 실행시켰을 때 save point 지점부터 다시 처리되도록 할 수 있다. (save point를 관리하기 힘들기 때문에 Task를 멱등성있게 개발하는 것이 더 좋은 대안 일 수 있다.) Kafka를 메시지 큐로 이용하는 Consumer라면 어플리케이션이 종료될 때 어디까지 메시지를 처리하였는지 offset을 commit하거나 별도로 저장 및 관리하여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Spring Boot 2.3이상부터는 web server를 종료할 때 graceful shutdown 할지 즉시 종료시킬지 정하는 property가 있다. &lt;b&gt;application.properties&lt;/b&gt;에 &lt;i&gt;&lt;b&gt;server.shutdown=graceful&lt;/b&gt;&lt;/i&gt;을 명시하면 application을 graceful하게 종료시킬 수 있다. Tomcat, Jetty, Undertow, Netty 네 가지 타입의 embedded servlet container는 graceful shutdown을 지원한다. application의 종료 절차가 시작되면 Tomcat, Jetty, Netty는 network layer에서 요청을 더 이상 받지 않도록 처리하며 Undertow인 경우에는 새로운 요청을 계속해서 받지만 즉시 503 (Service Unavailable)응답을 전달한다. default 값은 &lt;i&gt;&lt;b&gt;server.shutdown=immediate&lt;/b&gt;&lt;/i&gt;이다. server를 graceful하게 종료시킬 경우, 이미 진행 중인 요청을 처리하는데 데드락이 발생하여 어플리케이션이 종료되지 못하고 hang이 걸릴 수 있기 때문에 timeout을 설정해주는 것도 고려하여야한다. Spring Boot에서는 &lt;b&gt;application.properties&lt;/b&gt;에&amp;nbsp;&lt;i&gt;&lt;b&gt;spring.lifecycle.timeout-per-shutdown-phase=1m을 &lt;/b&gt;&lt;/i&gt;명시하여 shutdown하는 데&amp;nbsp; 필요한 시간을 설정할 수 있다. Spring Boot 2.3 이상 부터는 이런 메커니즘을 지원하지만 그렇지 않은 경우에는 어플리케이션이 안전하게 종료하는 방법을 찾아보아야한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위에서 말한 property를 설정한다면 Tomcat 기준으로 &lt;b&gt;org.springframework.boot.web.embedded.tomcat&lt;/b&gt; 패키지에 있는 &lt;b&gt;GracefulShutdown&lt;/b&gt; 클래스를 확인하면된다. 해당 클래스에는 &lt;b&gt;shutDownGracefully(GracefulShutdownCallback callback)&lt;/b&gt; 메서드가 있다. 해당 메서드는 &lt;b&gt;TomcatWebServer&lt;/b&gt; 클래스에서 호출하며 &lt;b&gt;TomcatWebServer&lt;/b&gt; 클래스는 &lt;b&gt;WebServerGracefulShutdownLifecycle&lt;/b&gt; 클래스에서 의존성을 갖고 있다. &lt;b&gt;WebServerGracefulShutdownLifecycle&lt;/b&gt;는 &lt;b&gt;ServletWebServerApplicationContext&lt;/b&gt; 클래스에서 singleton bean으로 등록되며 위에서 언급한 Spring Context가 종료될때 shutdown-hook으로 등록된 Thread에서 호출하는 &lt;b&gt;AbstractApplicationContext&lt;/b&gt; 클래스의 &lt;b&gt;doClose()&lt;/b&gt; 메서드가 호출되어 정리되는 bean들 중 하나이다. 즉, Spring Context가 JVM이 종료될 때 호출하는 shutdown-hook으로 등록한 Task들 중의 &lt;b&gt;GracefulShutDown&lt;/b&gt; 클래스의 &lt;b&gt;shutDownGracefully(GracefulShutdownCallback callback)&lt;/b&gt; 메서드도 포함된다. 상세 로직은 해당 클래스의 메서드들을 참고해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장황한 글이 된 것 같아 이해하기 어려울 수 있지만 이 글을 통해 전달하고자 하는 부분은 개발자는 &lt;i&gt;&lt;b&gt;어플리케이션의 시작과 실행 중인 상황 뿐만아니라 종료되는 시점도 잘 고려해야한다는 것&lt;/b&gt;&lt;/i&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 자료&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바 병렬 프로그래밍 chapter 7.4&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-boot-web-server-shutdown&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.baeldung.com/spring-boot-web-server-shutdown&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1621516052325&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;Web Server Graceful Shutdown in Spring Boot | Baeldung&quot; data-og-description=&quot;Learn how to take advantage of the new graceful shutdown feature in Spring Boot 2.3&quot; data-og-host=&quot;www.baeldung.com&quot; data-og-source-url=&quot;https://www.baeldung.com/spring-boot-web-server-shutdown&quot; data-og-url=&quot;https://www.baeldung.com/spring-boot-web-server-shutdown&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oDS4b/hyKhxY5KR1/pxVH7ktzIdCtdVqs1cM761/img.jpg?width=952&amp;amp;height=498&amp;amp;face=0_0_952_498,https://scrap.kakaocdn.net/dn/eynBb/hyKgLEItrr/kNhkEO0PKranKtiGr9pTh1/img.jpg?width=952&amp;amp;height=498&amp;amp;face=0_0_952_498&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/spring-boot-web-server-shutdown&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.baeldung.com/spring-boot-web-server-shutdown&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oDS4b/hyKhxY5KR1/pxVH7ktzIdCtdVqs1cM761/img.jpg?width=952&amp;amp;height=498&amp;amp;face=0_0_952_498,https://scrap.kakaocdn.net/dn/eynBb/hyKgLEItrr/kNhkEO0PKranKtiGr9pTh1/img.jpg?width=952&amp;amp;height=498&amp;amp;face=0_0_952_498');&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;Web Server Graceful Shutdown in Spring Boot | Baeldung&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how to take advantage of the new graceful shutdown feature in Spring Boot 2.3&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.baeldung.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;http://programmergamer.blogspot.com/2013/05/clarification-on-sigint-sigterm-sigkill.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://programmergamer.blogspot.com/2013/05/clarification-on-sigint-sigterm-sigkill.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1621516279343&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;Clarification on SIGKILL, SIGTERM, SIGINT, SIGQUIT, SIGSTP and SIGHUP&quot; data-og-description=&quot;A few days ago, i landed upon unix signals that lead to process termination. I guess i was trying to remember the signals generated in linu...&quot; data-og-host=&quot;programmergamer.blogspot.com&quot; data-og-source-url=&quot;http://programmergamer.blogspot.com/2013/05/clarification-on-sigint-sigterm-sigkill.html&quot; data-og-url=&quot;http://programmergamer.blogspot.com/2013/05/clarification-on-sigint-sigterm-sigkill.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;http://programmergamer.blogspot.com/2013/05/clarification-on-sigint-sigterm-sigkill.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;http://programmergamer.blogspot.com/2013/05/clarification-on-sigint-sigterm-sigkill.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Clarification on SIGKILL, SIGTERM, SIGINT, SIGQUIT, SIGSTP and SIGHUP&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A few days ago, i landed upon unix signals that lead to process termination. I guess i was trying to remember the signals generated in linu...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmergamer.blogspot.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알쓸신잡</category>
      <author>코딩하는 오징어</author>
      <guid isPermaLink="true">https://effectivesquid.tistory.com/73</guid>
      <comments>https://effectivesquid.tistory.com/entry/JVM%EC%9D%98-%EC%A2%85%EB%A3%8C%EC%99%80-Graceful-Shutdown#entry73comment</comments>
      <pubDate>Wed, 19 May 2021 13:44:55 +0900</pubDate>
    </item>
    <item>
      <title>Anatomy Kafka Consumer#2</title>
      <link>https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer2</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021/02/23 - [Message Queue/Kafka] - Anatomy Kafka Consumer#1&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1614489619795&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Anatomy Kafka Consumer#1&quot; data-og-description=&quot;Kafka는 Message Queue 시스템으로 현재 수 많은 곳에서 데이터를 처리하기 위해 사용되고 있다. kafka는 간단하게 다음과 같은 Architecture로 구성되어있다. kafka를 잘 활용하려면 topic의 구성 요소인 parit&quot; data-og-host=&quot;effectivesquid.tistory.com&quot; data-og-source-url=&quot;https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer1&quot; data-og-url=&quot;https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qN45a/hyJpyrW9Zs/vHxf8sw91GMGikXAJjPvl0/img.png?width=800&amp;amp;height=911&amp;amp;face=0_0_800_911,https://scrap.kakaocdn.net/dn/ckBM2X/hyJpHI9Wwh/z6A41BzvkaF2rKjMTbNPE1/img.png?width=800&amp;amp;height=911&amp;amp;face=0_0_800_911,https://scrap.kakaocdn.net/dn/b5un0A/hyJpJ1jCRc/Fcpegbsb0Znd4PfqEF2zGk/img.jpg?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512&quot;&gt;&lt;a href=&quot;https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qN45a/hyJpyrW9Zs/vHxf8sw91GMGikXAJjPvl0/img.png?width=800&amp;amp;height=911&amp;amp;face=0_0_800_911,https://scrap.kakaocdn.net/dn/ckBM2X/hyJpHI9Wwh/z6A41BzvkaF2rKjMTbNPE1/img.png?width=800&amp;amp;height=911&amp;amp;face=0_0_800_911,https://scrap.kakaocdn.net/dn/b5un0A/hyJpJ1jCRc/Fcpegbsb0Znd4PfqEF2zGk/img.jpg?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Anatomy Kafka Consumer#1&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;Kafka는 Message Queue 시스템으로 현재 수 많은 곳에서 데이터를 처리하기 위해 사용되고 있다. kafka는 간단하게 다음과 같은 Architecture로 구성되어있다. kafka를 잘 활용하려면 topic의 구성 요소인 parit&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;effectivesquid.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지난 글에 이어 kafka의 consumer를 개발할 때 필요한 지식 및 전략들을 알아보자. &lt;span style=&quot;color: #333333;&quot;&gt;지난 글 1편을 읽어보고 이글을 읽는 것이 학습에 더 큰 도움이 될 것이다. &lt;/span&gt;이 글은 다음과 같은 순서로 구성되어 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;kafka consumer가 broker에게 보내는 읽기 요청&lt;/li&gt;
&lt;li&gt;offset commit 전략&lt;/li&gt;
&lt;li&gt;rebalancing이 발생 했을 때의 handling&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;kafka consumer가 broker에게 보내는 읽기 요청&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;kafka broker는 client(producer, consumer)와 partition replica 및 controller로부터 partition leader에게 전송되는 요청을 처리하는 일을 한다. kafka는 TCP로 전송되는 이진 프로토콜(binary protocol)을 갖고 있다. 이 프로토콜은 요청의 형식을 규정하고 있으며, 또한 요청이 성공적으로 처리되거나 처리 중 에러가 발생했을 때 broker가 응답하나는 방법도 나타낸다. producer나 consumer application을 개발할 때는 kafka가 규정하고 있는 binary protocol을 직접 작성하지 않는다. client library가 low level(protocol 변환등)을 처리해주기 때문이다. 우리는 client library를 이용하여 객체를 메시지화해서 보내고, 신뢰성있는 데이터 전달을 위한 알고리즘을 작성하면 된다. 하지만 kafka consumer가 broker에게 실제로 어떤 요청을 하고 어떤 데이터를 응답 받는지 알아두면 kafka consumer의 동작을 이해하는데 큰 도움이 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;kafka broker는 특정 client로부터 받은 요청을 항상 수신된 순서로 처리한다. kafka가 메시지 큐처럼 동작하게 되어 저장되는 메시지의 순서가 보장된다. 모든 요청은 다음 내용을 포함하는 표준 헤더를 갖는다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;요청 타입 ID:&amp;nbsp;&lt;/b&gt;어떤 요청인지를 나타내며 16비트 정수 형식의 고유 번호다. 예를 들어, 카프카에 메시지를 쓰는 요청은 Produce라고 하며 이것의 ID값은 0이고, 메시지를 읽는 요청은 Fetch라고 하며 이것의 ID값은 1이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;요청 버전:&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 이 요청의 protocol API 버전을 나타내는 16비트 정숫값이다. 따라서 서로 다른 protocol 버전을 사용하는 kafka client를 broker가 처리하고 그에 맞춰 응답할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cID(coreelation ID):&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 사용자가 지정한 32비트 정숫값이며, 이것을 각 요청의 고유 식별 번호로 사용하면 문제를 해결하는 데 도움이 될 수 있다. 이 값은 응답과 에러 로그에도 표시된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;client ID:&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 사용자가 지정한 문자열 형식의 값이며 null이 될 수 있다. 이것은 요청을 전송한 client application을 식별하는 데 사용될 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;표준 헤더의 요청 타입에 따라 서로 다른 구조의 payload를 전송한다. 예를들어, kafka에 메시지를 쓰는 Produce요청의 경우 topic 이름과 partition ID 및 데이터등이 포함된다. 자세한 내용은 &lt;a href=&quot;http://kafka.apache.org/protocol.html&quot;&gt;kafka.apache.org/protocol.html&lt;/a&gt;를 참고하자.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&amp;nbsp;kafka consumer client는 읽기를 원하는 메시지의 topic과 partition 및 offset을 읽기 요청을 통해 broker에게 전달한다.&amp;nbsp;&lt;/b&gt;이에 더해 client는 각 partition마다 broker가 반환할 수 있는 데이터 크기를 제한할 수 있다. client는 broker가 전송한 응답을 저장하는 메모리를 할당해야 하므로 데이터 크기 제한이 중요하다. 크기를 제한하지 않으면 client의 메모리 부족을 초래할 만큼 큰 응답을 broker가 전송할 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;kafka의 읽기 및 쓰기 요청은 모두 partition의 leader가 처리하므로 partition leader에게 요청들이 전송되어야한다. 따라서 읽기 요청이 올바르게 전달되도록 client는 필수적인 meta data를 요청한다. &lt;span style=&quot;color: #333333;&quot;&gt;(partition leader가 down되어 leader를 선출하는 중이라면 kafka leader not available이라는 응답을 받을 수 있다. 이때는 재시도를 하면 대부분 문제가 해결된다.)&lt;/span&gt; 주의 할점은 kafka consumer가 partition leader에 존재하는 모든 메시지를 읽을 수 있는 것은 아니다라는 점이다. &lt;b&gt;대부분의 client는 모든 동기화 replica(ISR: in-sync replica)에 쓴 메시지들만 읽을 수 있다.&lt;/b&gt;(단, follower replica들 역시 consumer지만 예외다. 그렇지 않으면 복제를 할 수 없기 때문이다.) partition leader는 어떤 메시지들이 어느 replica에 복제되었는지 안다. 그리고 모든 동기화 replica들이 메시지를 쓸 때까지는 consumer에게 전송하지 않는다. kafka는 이러한 메커니즘을 이용하여 신뢰성있는 메시지 전달을 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Offset Commit 전략&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;kafka consumer를 개발할 때 offset commit을 위하여 다양한 전략을 취할 수 있다. 처리한 메시지의 offset에대한 commit을 제대로 수행하지 않는다면 이후 consumer rebalancing이 발생했을 때 메시지를 중복 처리하거나 메시지를 누락 시킬 가능성이 있기 때문이다. offset은 kafka 0.9 이전 버전 까지는 zookeeper에 저장되었다가 0.9버전 부터는 kafka에 저장되는 형태로 바뀌었다. 하지만 kafka broker의 버전이 0.9보다 높다고 하더라도 kafka client API의 버전에 따라 offset이 zookeeper에 저장될 수도 있다. old 버전의 kafka client도 new kafka broker와 통신이 가능하고 new 버전의 통신 프로토콜을 구현하지 않았다면 zookeeper에 저장될 것이다. commit 전략은 다음과 같이 구성할 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Auto Commit&lt;/li&gt;
&lt;li&gt;동기적인 Manual Commit&lt;/li&gt;
&lt;li&gt;비동기적인 Manual Commit&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Auto Commit&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;kafka consumer에서 offset을 commit하는 데 가장 쉬운 방법은 &lt;b&gt;enable.auto.commit&lt;/b&gt;을 활성화하여 Auto Commit을 이용하는 것이다. Auto Commit의 간격은 &lt;b&gt;auto.commit.interval.ms&lt;/b&gt;를 통해 설정이 가능하며 default 값은 5초이다. Auto Commit을 사용할 때는 몇 가지 주의 할 점이 있다.&amp;nbsp;&lt;i&gt;&lt;b&gt;첫 번째로&lt;/b&gt;&lt;/i&gt;&amp;nbsp;consumer에서 메시지를 commit을 하기 위해서는 poll() 메서드를 호출해야한다. 즉, Auto Commit은 consumer에서 poll() 메서드를 호출 했을 때 &lt;b&gt;auto.commit.interval.ms&lt;/b&gt;에 설정된 값을 기준으로 현재 commit을 할 시간이 되었는지 확인하여 commit할 시간이 되었다면 offset을 commit한다. 어떠한 이유로 poll() 메서드를 호출 한 이후 polling loop에서 코드가 block되어 다음 poll()메서드를 호출하지 못한다면 &lt;b&gt;auto.commit.interval.ms&lt;/b&gt;에 설정된 시간이 지나도 offset이 commit되지 않는다.&amp;nbsp;&lt;i&gt;&lt;b&gt;두 번째로&lt;/b&gt;&lt;/i&gt;&amp;nbsp;어떠한 이유로 consumer가 rebalancing되었을 때 &lt;b&gt;auto.commit.interval.ms&lt;/b&gt;에 설정된 값에 따라 메시지를 중복 처리하게 될 수 있다. 예를 들어, Auto Commit이 5초마다 수행되도록 설정하고, 가장 최근의 마지막 commit을 한 후에 3초 동안 추가로 메시지들을 읽고 처리하다가 rebalancing이 시작되었다고 가정해보자. rebalancing이 끝난 후 모든 consumer들은 마지막으로 commit된 offset부터 메시지들을 읽기를 시작할 것이다. 이 경우 그 offset은 3초 이전의 것이므로, 3초 동안에 읽었던 모든 메시지는 중복으로 처리될 것이다. Auto Commit이 자주 실행되도록 &lt;b&gt;auto.commit.interval.ms&lt;/b&gt; 값을 작게 조정할 수 있지만 완벽하게 해결할 수 있지는 않다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;161&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VHRIU/btqYB6JlDHY/UyoFcEFQs4eHm0wrHQJKM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VHRIU/btqYB6JlDHY/UyoFcEFQs4eHm0wrHQJKM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VHRIU/btqYB6JlDHY/UyoFcEFQs4eHm0wrHQJKM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVHRIU%2FbtqYB6JlDHY%2FUyoFcEFQs4eHm0wrHQJKM0%2Fimg.png&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;161&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위의 그림을 보면 0, 3, 6, 9 메시지들이 중복으로 처리되게 되는 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;동기적인 Manual Commit&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;enable.auto.commit=false로 설정하면 application에서 요구할 때만 offset이 commit된다. 이때 가장 간단하면서도 신뢰도가 높은 것이 commitSync() 메서드를 호출하는 것이다. 이 메서드는 poll() 메서드에서 반환된 마지막 offset을 commit한다. 그리고 offset이 성공적으로 commit되면 실행이 끝지만, 어떠한 이유로 commit에 실패하면 예외를 발생시킨다. commitSync() 메서드는 poll()에서 반환된 가장 최근의 offset을 commit한다는 것에 유의해야한다. 따라서 poll()에서 반환된 모든 메시지들이 처리가 다 된 후에 commitSync() 메서드를 호출해야한다. 그렇지 않으면 메시지를 누락시킬 가능성이 생긴다. commitSync() 예제 코드는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1614486547549&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while (flag.get()) {
    ConsumerRecords&amp;lt;String, String&amp;gt; records = consumer.poll(Duration.ofSeconds(3)); //3초동안 읽을 데이터를 계속 fetch하다가 없으면 empty collection을 return

    for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
        Map&amp;lt;String, String&amp;gt; value = objectMapper.readValue(record.value(), new TypeReference&amp;lt;&amp;gt;() {});
        log.info(&quot;message: {}&quot;, value);

        Optional.ofNullable(value.get(&quot;stop&quot;))
            .ifPresent(isStop -&amp;gt; {
                if (Boolean.parseBoolean(isStop)) {
                    flag.set(false);
                }
            });
    }

    consumer.commitSync(); // poll() 메서드에서 반환된 메시지들 중 마지막 메시지의 offset을 commit한다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;비동기적인 Manual Commit&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;broker가 commit 요청에 응답할 때까지 application이 block된다는 것이 동기적인 Manual Commit의 단점이다. 이로 인해 consumer의 처리량이 감소하게 되고 producer에서 메시지 발행이 빨라 진다면 Lag가 발생하게 된다. 이를 해결하기 위해 kafka consumer client api는 &lt;span style=&quot;color: #333333;&quot;&gt;commitAsync() 메서드를 통해&amp;nbsp;&lt;/span&gt;비동기적인 Manual Commit을 제공한다.&lt;/p&gt;
&lt;pre id=&quot;code_1614486838645&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while (flag.get()) {
    ConsumerRecords&amp;lt;String, String&amp;gt; records = consumer.poll(Duration.ofSeconds(3)); //3초동안 읽을 데이터를 계속 fetch하다가 없으면 empty collection을 return

    for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
        Map&amp;lt;String, String&amp;gt; value = objectMapper.readValue(record.value(), new TypeReference&amp;lt;&amp;gt;() {});
        log.info(&quot;message: {}&quot;, value);

        Optional.ofNullable(value.get(&quot;stop&quot;))
            .ifPresent(isStop -&amp;gt; {
                if (Boolean.parseBoolean(isStop)) {
                    flag.set(false);
                }
            });
    }

    consumer.commitAsync(); // 마지막 offset을 비동기적으로 commit하고 다음 코드를 진행시킨다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;commit이 성공하거나 재시도 불가능한 에러가 발생할 때까지 commitSync() 메서드는 commit을 재시도하지만, commitAsync() 메서드는 재시도하지 않는다는 것이 단점이다. commitAsync() 메서드에서 broker의 응답을 받는 사이에 이후의 다른 commit이 먼저 성공할 수 있기 때문이다. 예를 들어, offset 2000을 commit하는 요청을 전송했는데 일시적인 통신 문제가 생겨 broker가 그 요청을 받지 못해 응답 할 수 없다고 가정해보자. 그리고 그 동안에 또 다른 배치를 처리하고 offset 3000을 성공적으로 commit하였다. 이때 만일 commitAsync() 메서드에서 이전에 실패했던 commit을 재시도한다면 offset 2000의 commit이 성공할 수 있을 것이다. 그러나 이것은 offset 3000이 이미 처리되고 commit된 이후다. 따라서 rebalancing이 발생된다면 더 많은 메시지의 중복 처리가 생길 것이다. 따라서 offset commit의 순서를 지키는 것은 중요하다. offset commit의 순서가 바뀌는 경우는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;commitAsync() 메서드를 호출 할 때 callback 메서드를 정의하여 전달하며 callback 메서드에서 commit이 실패하였을 때 재시도를 수행할 경우&lt;/li&gt;
&lt;li&gt;처리량을 높이기위해 multi-thread를 통해 메시지를 처리하고 offset을 commit 할 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;multi-thread를 통해 메시지를 처리하는 전략에 대해서도 긴 글이 될 수 있으므로 이 부분은 다른 글을 통해서 소개해보도록 하고 이 글에서는 callback 메서드에서 재시도를 할 경우에 대해서만 다루어 보겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1614487654675&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while (flag.get()) {
    ConsumerRecords&amp;lt;String, String&amp;gt; records = consumer.poll(Duration.ofSeconds(3)); //3초동안 읽을 데이터를 계속 fetch하다가 없으면 empty collection을 return

    for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
        Map&amp;lt;String, String&amp;gt; value = objectMapper.readValue(record.value(), new TypeReference&amp;lt;&amp;gt;() {});
        log.info(&quot;message: {}&quot;, value);

        Optional.ofNullable(value.get(&quot;stop&quot;))
            .ifPresent(isStop -&amp;gt; {
                if (Boolean.parseBoolean(isStop)) {
                    flag.set(false);
                }
            });
    }

    consumer.commitAsync((offsets, exception) -&amp;gt; {
        if (exception != null) {
            log.error(&quot;commit fail!! msg: {}&quot;, exception.getMessage(), exception);
            // 여기서 재시도 로직을 넣게되면 문제가 발생할 수 있다.
        }
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 코드와 같이 commitAsync() 메서드에는 callback 메서드를 전달 할 수 있다. 이 callback 메서드에 commit실패시 재시도 로직을 넣는다면 위에서 말한 문제가 발생할 수 있다. (callback 메서드는 broker에서 응답을 받았을 때 실행된다.) callback 메서드에서 재시도 로직을 넣어야한다면 다음과 같은 방법으로 offset commit요청 순서를 지킬 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;순차적으로 증가하는 일련번호를 정의한다. (ex. AtomicInteger)&lt;/li&gt;
&lt;li&gt;commit할 때마다 일련번호를 증가시키고 그것을 callback 메서드에 전달한다.&lt;/li&gt;
&lt;li&gt;재시도 commit을 전송하기 전 callback이 갖고 있던 일련번호와 외부의 일련번호를 비교한다.&lt;/li&gt;
&lt;li&gt;두 일련번호가 같다면 더 새로운 commit이 없었으므로 재시도를 해도 안전하다. 그러나 외부의 일련번호가 더 크다면 재시도를 하지말아야한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위와 같이 CAS연산과 비슷한 방식을 이용하면 commitAsync() 메서드를 이용할 때도 재시도 전략을 가져갈 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Rebalancing이 발생 했을 때의 handling&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;Rebalancing이란 지난 글에서 소개했듯이 consumer들의 topic의 partition 소유권을 재정비하는 메커니즘이다. rebalancing이 발생하는 이유는 지난 글에서 다루었으므로 이 글에서는 rebalancing이 발생했을 때 어떻게 handling해야할지와 그 방법을 소개해보려고한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;consumer는 종료되기 전이나 partition rebalancing이 시작되기 전에 사용하던 자원들을 정리하고 마지막으로 처리한 메시지의 offset을 commit해야한다. kafka consumer API에는 consumer에 partition이 추가할당 되거나 제거될 때 코드가 실행 될 수 있도록 &lt;b&gt;ConsumerRebalanceListener&lt;/b&gt; 인터페이스를 제공한다. subscribe() 메서드를 호출 할 때 &lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;ConsumerRebalanceListener&lt;/b&gt;를 구현한 객체를 인자로 전달하면 partition이 추가로 할당되거나 제거될 때 우리가 원하는 동작을 실행시킬 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ConsumerRebalanceListener에는 다음과 같은 메서드들이 선언되어 있다.&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: #333333;&quot;&gt;&lt;b&gt;public void onPartitionRevoked(Collection&amp;lt;TopicPartition&amp;gt; partitions):&lt;/b&gt; rebalancing이 시작되기 전에, 그리고 consumer가 메시지 소비를 중단한 후 호출된다. 처리된 마지막 메시지의 offset을 commit하고 사용하던 자원을 정리해야 하는 곳이 바로 이 메서드이다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;public void onPartitionAssigned(Collection&amp;lt;TopicPartition&amp;gt; partitions):&lt;/b&gt; partition이 broker에게 재할당된 후, 그리고 consumer가 partition을 새로 할당받아 메시지 소비를 시작하기 전에 호출된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1614489249278&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ConsumerRebalanceListener rebalanceListener = new ConsumerRebalanceListener() {
    @Override
    public void onPartitionsRevoked(Collection&amp;lt;TopicPartition&amp;gt; partitions) {
        log.info(&quot;onPartitionsRevoked: {}&quot;, partitions);
    }

    @Override
    public void onPartitionsAssigned(Collection&amp;lt;TopicPartition&amp;gt; partitions) {
        log.info(&quot;onPartitionsAssigned: {}&quot;, partitions);
    }
};

consumer.subscribe(topics, rebalanceListener);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;여기 까지 내용을 살펴봤다면 kafka의 consumer를 개발할 때 고민해야할 부분들을 어느정도 알고 있다고 해도 무방할 것이다. 물론 topic의 partition들의 복제를 kafka broker에서 어떻게 다루는지 partition leader선출과 leader가 down되었을 때 어떤 메커니즘으로 복구되는지도 알아야한다. 또한 consumer의 처리량을 높이기 위해 메시지를 병렬로 처리할 때 주의 할 점에 대해서도 학습 할 필요가 있다. 해당 내용들은 다른 글을 통해 소개해보도록 하고 kafka의 consumer를 개발할 때 기본적으로 고려해야할 부분들은 여기서 마치도록 하겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고 자료:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Apache Kafka 핵심 가이드&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;https://stackoverflow.com/questions/41137281/offsets-stored-in-zookeeper-or-kafka&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;stackoverflow.com/questions/41137281/offsets-stored-in-zookeeper-or-kafka&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@durgaswaroop/a-practical-introduction-to-kafka-storage-internals-d5b544f6925f&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;medium.com/@durgaswaroop/a-practical-introduction-to-kafka-storage-internals-d5b544f6925f&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Message Queue/Kafka</category>
      <author>코딩하는 오징어</author>
      <guid isPermaLink="true">https://effectivesquid.tistory.com/72</guid>
      <comments>https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer2#entry72comment</comments>
      <pubDate>Sun, 28 Feb 2021 14:19:29 +0900</pubDate>
    </item>
    <item>
      <title>Anatomy Kafka Consumer#1</title>
      <link>https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer1</link>
      <description>&lt;p&gt;Kafka는 Message Queue 시스템으로 현재 수 많은 곳에서 데이터를 처리하기 위해 사용되고 있다. kafka는 간단하게 다음과 같은 Architecture로 구성되어있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;455&quot; width=&quot;257&quot; height=&quot;NaN&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kEhep/btqYhyScxCD/S89uLUNnBJPpnhFVfN6ch1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kEhep/btqYhyScxCD/S89uLUNnBJPpnhFVfN6ch1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kEhep/btqYhyScxCD/S89uLUNnBJPpnhFVfN6ch1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkEhep%2FbtqYhyScxCD%2FS89uLUNnBJPpnhFVfN6ch1%2Fimg.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;455&quot; width=&quot;257&quot; height=&quot;NaN&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;kafka를 잘 활용하려면 topic의 구성 요소인 paritition들에 메시지가 어떻게 저장되는지, partition들의 replica가 broker들 사이에서 어떻게 관리되는지, 가용성과 메시지의 신뢰성있는 전달을 위해 producer application에서 다뤄야하는 option값들은 어떤 것들이 있는지 등 살펴보아야 할 것 들이 많다. 이러한 내용들은 차차 다뤄 보기로 하고, 이 글에서는 kafka client application중 하나인 consumer가 어떻게 동작하는지 자세히 살펴 볼 것이다. 이 글은 kafka에서 사용되는 기본적인 용어들을 알고 있다는 전제하에 작성하였다. 먼저, 일반적인 Message Queue(이하 MQ) 시스템에서 consumer는 producer가 발행한 메시지를 소비하는 application이다. kafka에서의 consumer도 같은 역할을 하지만, 타 MQ시스템과 달리 kafka의 consumer는 메시지를 push 방식이 아닌 polling 방식으로 소비한다. 이와 같은 메커니즘 덕분에 topic에 쌓이는 메시지가 필요한 곳, 어디든 consumer를 개발하여 타 시스템과 독립적으로 메시지를 소비하여 비즈니스 로직을 처리할 수 있다. 이렇게 동작 될 수 있도록 필요한 값이 바로 &lt;b&gt;group.id&lt;/b&gt;이다. kafka는 consumer group단위로 partition의 메시지 위치인 offset을 관리한다. offset을 관리한다는 말은 consumer가 메시지를 어디까지 소비하였는지 관리한다는 뜻이다. consumer group에 join되는 consumer의 수에는 제한이 없지만 하나의 partition은 하나의 consumer만이 메시지를 소비할 수 있다. &lt;b&gt;즉, 두 개 이상의 consumer가 하나의 partition에 있는 메시지를 소비하지 못한다.&lt;/b&gt; (하나의 consumer가 하나의 partition만 할당 받는 것은 아니다. 하나의 consumer는 여러개의 partition을 할당 받아 메시지를 소비할 수 있다.) 그럼 다음 순서로 kafka의 consumer가 어떻게 동작하는지 살펴보자.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;partition과 consumer&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지 순서&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;consumer 동작에 영향을 주는 parameter&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;polling loop&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;partition과 consumer&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;265&quot; width=&quot;350&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XHUuG/btqX819igp6/7yH4sF3QQhVx6Hz8ckETdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XHUuG/btqX819igp6/7yH4sF3QQhVx6Hz8ckETdK/img.png&quot; data-alt=&quot;case 1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XHUuG/btqX819igp6/7yH4sF3QQhVx6Hz8ckETdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXHUuG%2FbtqX819igp6%2F7yH4sF3QQhVx6Hz8ckETdK%2Fimg.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;265&quot; width=&quot;350&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;case 1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;254&quot; width=&quot;350&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKH8q8/btqYiCz5EI5/r0Jcme4nOGvTfebUJQbvM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKH8q8/btqYiCz5EI5/r0Jcme4nOGvTfebUJQbvM1/img.png&quot; data-alt=&quot;case 2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKH8q8/btqYiCz5EI5/r0Jcme4nOGvTfebUJQbvM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKH8q8%2FbtqYiCz5EI5%2Fr0Jcme4nOGvTfebUJQbvM1%2Fimg.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;254&quot; width=&quot;350&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;case 2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;218&quot; width=&quot;350&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5CMGw/btqYeaxPSDw/6zQjukbviNcDOfOg974vqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5CMGw/btqYeaxPSDw/6zQjukbviNcDOfOg974vqK/img.png&quot; data-alt=&quot;case 3&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5CMGw/btqYeaxPSDw/6zQjukbviNcDOfOg974vqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5CMGw%2FbtqYeaxPSDw%2F6zQjukbviNcDOfOg974vqK%2Fimg.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;218&quot; width=&quot;350&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;case 3&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위의 그림을 살펴보자. case 1은 하나의 topic에 4개의 partition이 존재한다. 그리고 두 개의 consumer가 join된 그룹에서 해당 topic을 구독하고 있다. partition들을 모두 소비해야하므로 하나의 consumer가 partition을 두개 씩 할당 받아 메시지를 소비하게된다. 이 때 특정 consumer가 특정 partition만을 소비할 수 있도록 할 수 있지만 일반적인 rebalancing 알고리즘은 round robin으로 균형을 맞춘다. &lt;b&gt;(rebalancing이 발생할 경우 모든 consumer가 메시지 소비를 중단하므로 rebalancing이 자주 일어나게 해서는 안된다.)&lt;/b&gt;case 1의 상황에서 메시지 소비 속도가 메시지 발행 속도보다 뒤쳐진다면 case 2처럼 consumer를 scale out 하여 처리량을 높일 수 있다. consumer를 무한정 scale out한다고해서 처리량이 높아지는 것은 아니다. case 3 처럼 consumer를 늘려도 partition은 하나의 consumer에서만 소비되므로 4개의 consumer는 대기 상태로 있게된다. 4개의 consumer도 활성화 시키려면 partition수를 8개로 늘려주면 된다. 하지만 partition수는 늘리기만 가능하고, 줄일 수 없기 때문에 &lt;span style=&quot;color: #333333;&quot;&gt;partition 수는&amp;nbsp;&lt;/span&gt;신중하게 조절해야한다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;메시지 순서&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;600&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZYgC5/btqYfWeW25H/HTJTAJjSp5hiEWlSOFkj4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZYgC5/btqYfWeW25H/HTJTAJjSp5hiEWlSOFkj4k/img.png&quot; data-alt=&quot;partition&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZYgC5/btqYfWeW25H/HTJTAJjSp5hiEWlSOFkj4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZYgC5%2FbtqYfWeW25H%2FHTJTAJjSp5hiEWlSOFkj4k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;600&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;partition&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;하나의 topic에 세 개의 partition이 존재한다고 가정해보자. 메시지를 발행할 때 key값을 별도로 주지 않는다면 메시지는 round robin 알고리즘으로 partition에 분배된다. consumer는 partition에서 메시지를 읽을 때 commit되지 않은 offset부터 순서대로 읽는다. 즉 메시지가 처리될 때 각 partition에 존재하는 메시지들에 대한 읽는 순서가 보장된다. 예를 들어 메시지 0과 1은 읽는 순서가 보장되지 않지만(partition이 다르므로) 메시지 0과 3은 메시지 읽는 순서가 보장되어 처리된다. 여기서 읽는 순서라고 강조한 것은 처리 순서를 보장하지는 않기 때문이다. 처리 순서는 전적으로 consumer application에 달려있다. partition의 메시지들을 읽어 병렬로 처리하게 된다면 읽는 순서는 보장될지라도 처리 순서는 바뀔 수 있기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;consumer 동작에 영향을 주는 parameter&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;auto.offset.reset:&lt;/b&gt; commit된 offset이 없는 partition을 consumer가 읽기 시작 할 때, 또는 commit된 offset이 있지만 유효하지 않을 때, consumer가 어떤 record(메시지)를 읽게 할 것인지 제어하는 paramter이다. 필자는 메시지 누락(유실)을 처리하는 것 보다 메시지 중복을 처리하는 게 더 수월하다고 판단하여 earliest 옵션을 주어 개발하였다. 메시지 중복처리는 모든 메시지에 대해 idempotent하게 처리하거나 처리된 메시지는 다시 processing되지 않게 방어 로직을 추가하는 방법등이 있다.
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;latest: default 값으로 가장 최근의 record들을 읽기 시작한다.&lt;/li&gt;
&lt;li&gt;earliest: 해당 partition의 맨 앞 부터 모든 record를 읽기 시작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;enable.auto.commit:&lt;/b&gt; default값은 true이며, consumer의 offset commit을 자동으로 할 것 인지 결정한다. 이 값이 true일 때는 &lt;b&gt;auto.commit.interval.ms&lt;/b&gt;를 설정하여 자동으로 offset을 commit하는 시간 간격을 제어 할 수 있다. 해당 동작은 consumer에서 poll()을 호출했을 때 이전 commit을 한 후 &lt;b&gt;auto.commit.interval.ms&lt;/b&gt;만큼 시간이 흘렀는지 체크한 후, 항상 이전 호출에서 반환된 마지막 offset을 commit한다. 단, &lt;b&gt;poll()&lt;/b&gt;이 호출 되지 않는다면 &lt;b&gt;auto.commit.interval.ms가 지나도 commit되지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;session.timeout.ms: &lt;/b&gt;default 값은 10초이며 consumer가 heartbeat를 전송하지 않고 살아있을 수 있는 시간이다. 만일, consumer가 GroupCoordinator에게 heartbeat를 전송하지 않으면서 &lt;b&gt;session.timeout.ms&lt;/b&gt;로 설정된 시간이 경과되면, 해당 consumer는 종료된 것으로 간주되고 GroupCoordinator는 해당 consumer가 담당하고 있는 partition들을 재분배 하기위해 rebalancing을 일으킨다. consumer의 poll() 메서드에서 GroupCoordinator에게 heartbeat를 전송하는 시간 간격을 제어하는 것이 &lt;b&gt;heartbeat.interval.ms&lt;/b&gt;이다. 그러므로 두 paramter를 같이 고려하여 설정해야한다. 즉, &lt;b&gt;heartbeat.interval.ms&lt;/b&gt;는 &lt;b&gt;session.timeout.ms&lt;/b&gt;의 값보다 작아야한다. 대개 &lt;b&gt;heartbeat.interval.ms&lt;/b&gt;를 &lt;b&gt;session.timeout.ms&lt;/b&gt;값의 1/3로 설정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;max.poll.interval.ms:&lt;/b&gt; consumer가 heartbeat를 정상적으로 보내 broker에게 health check를 하더라도 실제로 poll() 메서드를 호출하지 않는다면 해당 partition을 점유하면서 메시지는 처리하지 않는 상황이 발생한다. 이를 막기위해 &lt;b&gt;max.poll.interval.ms&lt;/b&gt;를 설정한다. 해당 시간이 지나도록 poll()메서드를 호출하지 않는다면 consumer가 메시지를 처리할 수 없는 상황이라고 판단하여 consumer group에서 제외 후 rebalancing이 발생한다. 하나의 메시지가 처리되는 시간을 고려하여 해당 값을 설정하여야한다. 메시지 처리가 오래 걸리는 비즈니스 로직이라면 해당 값을 크게 잡아주어야 불필요한 rebalancing이 발생하지 않기 때문이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;max.poll.records: &lt;/b&gt;consumer가 poll호출을 할 때마다 가져오는 record의 최대 갯수이다. partition에 메시지가 충분하다면 해당 parameter 설정 값 만큼 메시지를 읽어오지만 메시지 발행 속도가 느려 메시지가 부족하다면 해당 값 보다 적을 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;fetch.min.bytes:&lt;/b&gt; record들을 가져올 때 broker로부터 받기 원하는 데이터의 최소량(byte)을 설정한다. 만일 broker가 consumer로부터 record 요청을 받았지만 읽을 record들의 양이 fetch.min.bytes에 지정된 것보다 작다면, broker는 더 많은 메시지가 모일 때까지 기다렸다가 consumer에게 전송한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;fetch.max.wait.ms:&lt;/b&gt; 기본적으로 kafka는 500ms동안 fetch.min.bytes 만큼 record들의 양이 찰때까지 기다린다. 500ms가 지났는데도 데이터가 모이지 않는다면 더 이상 기다리지 않고 consumer에게 메시지를 보낸다. fetch.min.bytes를 1MB, fetch.max.wait.ms를 1000ms로 설정하였다면 kafka는 consumer의 데이터 읽기 요청을 받은 후 데이터 양이 1MB가 되거나 1000ms가 지난 후에 데이터를 전송할 것이다. producer의 &lt;b&gt;linger.ms&lt;/b&gt; paramter와 비슷하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이외에도 &lt;span&gt;&lt;b&gt;key.deserializer, &lt;/b&gt;&lt;span&gt;&lt;b&gt;value.deserializer, &lt;/b&gt;&lt;span&gt;&lt;b&gt;bootstrap.servers&lt;/b&gt;등의 기본 paramter들이 있지만 해당 paramter들은 어렵지 않으므로 kafka document를 확인해보자.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Polling Loop&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;polling loop는 consumer의 poll() 메서드를 호출하는 loop를 말한다. 해당 loop에서 poll() 메서드 호출을 통해 받아온 record들을 처리한다. 해당 부분은 예제 코드를 보는 것이 몇 번의 설명보다 낫다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1614011440405&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String... args) throws JsonProcessingException, InterruptedException {
    ObjectMapper objectMapper = new JacksonConfig().objectMapper();
    KafkaConsumer&amp;lt;String, String&amp;gt; consumer = new KafkaConsumer&amp;lt;&amp;gt;(consumerProps());
    List&amp;lt;String&amp;gt; topics = new ArrayList&amp;lt;&amp;gt;();
    topics.add(&quot;play.kafka&quot;);
    
    consumer.subscribe(topics);

    // 해당 loop를 polling loop라고 한다. 이 loop는 메시지를 읽어서 로그를 찍은 후 commit하는 loop이다.
    while (true) {
        ConsumerRecords&amp;lt;String, String&amp;gt; records = consumer.poll(Duration.ofSeconds(3)); //3초동안 읽을 데이터를 계속 fetch하다가 없으면 empty collection을 return

        for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
            Map&amp;lt;String, String&amp;gt; value = objectMapper.readValue(record.value(), new TypeReference&amp;lt;&amp;gt;() {});
            log.info(&quot;message: {}&quot;, value);
        }

        consumer.commitSync();
    }

    consumer.close();
}

private static Map&amp;lt;String, Object&amp;gt; consumerProps() {
    Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;localhost:9092&quot;);
    props.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;vanilla.consumer&quot;);
    props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;earliest&quot;);
    props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 3);
    props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 1000);

    return props;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 설명 이외에도 kafka의 consumer에 대해 알아두어야할 중요한 부분들이 많다. 글이 너무 길면 지루해질 수 있기 때문에 kafka consumer에대한 기본적인 내용은 여기서 마치겠다. consumer에서 offset을 manual하게 commit하는 방법과 commit전략, consumer에서 multi thread를 다루는 방법, 불필요한 rebalancing이 발생하지 않도록 하기위한 전략, consumer가 broker에 전달하는 읽기 요청, 실제 예제 코드 등 다양한 이야기는 2편에서 더 자세히 다루도록 하겠다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021/02/28 - [Message Queue/Kafka] - Anatomy Kafka Consumer#2&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1614489602170&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Anatomy Kafka Consumer#2&quot; data-og-description=&quot;2021/02/23 - [Message Queue/Kafka] - Anatomy Kafka Consumer#1 지난 글에 이어 kafka의 consumer를 개발할 때 필요한 지식 및 전략들을 알아보자. 지난 글 1편을 읽어보고 이글을 읽는 것이 학습에 더 큰 도움..&quot; data-og-host=&quot;effectivesquid.tistory.com&quot; data-og-source-url=&quot;https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer2&quot; data-og-url=&quot;https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer2&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/07c5y/hyJpC8XmPc/dnIaSiZm3K2TOZl9HKxODk/img.png?width=580&amp;amp;height=161&amp;amp;face=0_0_580_161,https://scrap.kakaocdn.net/dn/hTh6s/hyJpEsaMCK/3dIRhYCzAAqG8NuV80CEQ1/img.png?width=580&amp;amp;height=161&amp;amp;face=0_0_580_161,https://scrap.kakaocdn.net/dn/6aXN9/hyJpLdK200/j1yc2R4uG6wlkEK5XHG9w1/img.jpg?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512&quot;&gt;&lt;a href=&quot;https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer2&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/07c5y/hyJpC8XmPc/dnIaSiZm3K2TOZl9HKxODk/img.png?width=580&amp;amp;height=161&amp;amp;face=0_0_580_161,https://scrap.kakaocdn.net/dn/hTh6s/hyJpEsaMCK/3dIRhYCzAAqG8NuV80CEQ1/img.png?width=580&amp;amp;height=161&amp;amp;face=0_0_580_161,https://scrap.kakaocdn.net/dn/6aXN9/hyJpLdK200/j1yc2R4uG6wlkEK5XHG9w1/img.jpg?width=512&amp;amp;height=512&amp;amp;face=0_0_512_512');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Anatomy Kafka Consumer#2&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;2021/02/23 - [Message Queue/Kafka] - Anatomy Kafka Consumer#1 지난 글에 이어 kafka의 consumer를 개발할 때 필요한 지식 및 전략들을 알아보자. 지난 글 1편을 읽어보고 이글을 읽는 것이 학습에 더 큰 도움..&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;effectivesquid.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;참고 서적:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Apache Kafka 핵심 가이드&lt;/li&gt;
&lt;li&gt;카프카, 데이터 플랫폼의 최강자&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Message Queue/Kafka</category>
      <author>코딩하는 오징어</author>
      <guid isPermaLink="true">https://effectivesquid.tistory.com/71</guid>
      <comments>https://effectivesquid.tistory.com/entry/Anatomy-Kafka-Consumer1#entry71comment</comments>
      <pubDate>Tue, 23 Feb 2021 01:26:24 +0900</pubDate>
    </item>
  </channel>
</rss>