<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Traveling Programmer</title>
    <link>https://to-travel-coding.tistory.com/</link>
    <description>기록해가며 하는 공부</description>
    <language>ko</language>
    <pubDate>Tue, 7 Apr 2026 12:09:31 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>27200</managingEditor>
    <image>
      <title>Traveling Programmer</title>
      <url>https://tistory1.daumcdn.net/tistory/5926452/attach/4d5a193dc3a743dbbbf66a667c3b82a2</url>
      <link>https://to-travel-coding.tistory.com</link>
    </image>
    <item>
      <title>[Kafka] 당신의 카프카는 zero copy입니까?</title>
      <link>https://to-travel-coding.tistory.com/477</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;해당 내용의 글을 작성하며 &lt;br /&gt;1. Kafka를 로컬에서 SSL 인증을 경유하게 설정하는 방식.&lt;br /&gt;2. Kafka의 시스템 콜 로그를 찍는 방식.&lt;br /&gt;2가지에 대해 AI를 사용하였습니다. 짧은 지식으로 작성한 내용이기에 AI 사용 부분 혹은, 그 외의 어느 부분에서라도 오류가 존재한다면 댓글에 남겨주시면 감사하겠습니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;접근&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;u&gt;카프카는 왜 빠를까?&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카를 한 번이라도 사용해 본 적이 있는 사람이라면, 그 사람이 개발자라면 반드시 궁금했을만한 내용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 메모리도 아니고, 디스크를 사용한다는데 어떻게 그렇게 빠를까? 디스크는 느린 거 아닌가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 궁금증이 들어 내용을 찾아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수많은 블로그, 그리고 &lt;a href=&quot;https://kafka.apache.org/42/design/design/?utm_source=chatgpt.com#end-to-end-batch-compression&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 docs&lt;/a&gt; 까지 모두 대략 3가지의 근거를 들었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Sequential I/O&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 읽거나 쓸 때 Random Access I/O가 아닌 Sequential I/O를 제공함으로써, 디스크 헤드 동선을 최적화한다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;585&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qL3zU/dJMcahX9VZO/JcuYUQx1U5ZdqkgGylAWVK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qL3zU/dJMcahX9VZO/JcuYUQx1U5ZdqkgGylAWVK/img.jpg&quot; data-alt=&quot;출처 : https://deliveryimages.acm.org/10.1145/1570000/1563874/jacobs3.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qL3zU/dJMcahX9VZO/JcuYUQx1U5ZdqkgGylAWVK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqL3zU%2FdJMcahX9VZO%2FJcuYUQx1U5ZdqkgGylAWVK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;585&quot; height=&quot;322&quot; data-origin-width=&quot;585&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://deliveryimages.acm.org/10.1145/1570000/1563874/jacobs3.jpg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.&amp;nbsp; 파일 시스템을 사용하고 페이지 캐시에 의존한다.&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-4-thinking&quot; data-message-id=&quot;a3cebe74-2600-436f-a5cd-323b085fd6ad&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1103&quot; data-start=&quot;981&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Kafka는 데이터를 JVM 힙 캐시로 들고 있기보다 디스크 로그 + OS 페이지 캐시에 맡김으로써, 더 큰 캐시 효과를 얻고 GC 문제를 줄이며 높은 처리량을 유지하려는 설계를 택한 것&lt;/b&gt;이라고 이해하면 된다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1103&quot; data-start=&quot;981&quot; data-ke-size=&quot;size16&quot;&gt;문서의 내용을 GPT가 요약한 것이다. 자세한 내용이 궁금하면 위의 docs를 참고하자.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1103&quot; data-start=&quot;981&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1103&quot; data-start=&quot;981&quot; data-ke-size=&quot;size23&quot;&gt;3. zero copy&lt;/h3&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 364px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 364px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 364px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUzDKP/dJMcabcyiZs/Y6DE73BHQcLam5sEMRapL1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUzDKP/dJMcabcyiZs/Y6DE73BHQcLam5sEMRapL1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUzDKP/dJMcabcyiZs/Y6DE73BHQcLam5sEMRapL1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cUzDKP/dJMcabcyiZs/Y6DE73BHQcLam5sEMRapL1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;368&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 364px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2m7aF/dJMcaaxY1av/ysAnOaDHmrSjfHUPicGxH0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2m7aF/dJMcaaxY1av/ysAnOaDHmrSjfHUPicGxH0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2m7aF/dJMcaaxY1av/ysAnOaDHmrSjfHUPicGxH0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/2m7aF/dJMcaaxY1av/ysAnOaDHmrSjfHUPicGxH0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;350&quot; height=&quot;312&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;기존 방식&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;제로카피 방식&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제로 카피를 사용하면 컨텍스트 스위칭 비용이 줄어들고, 복사가 줄어든다. 심지어 제로카피 업그레이드 버전까지 사용한다면 복사가 2번만 일어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제로카피는 이러한 비용을 절감시킴으로서 성능 개선을 이루어낸다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lNoZk/dJMcagZe0dj/7Z5ikDfGr9gK4Vd0SKtdf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lNoZk/dJMcagZe0dj/7Z5ikDfGr9gK4Vd0SKtdf1/img.png&quot; data-alt=&quot;출처 : https://developer.ibm.com/articles/j-zerocopy/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lNoZk/dJMcagZe0dj/7Z5ikDfGr9gK4Vd0SKtdf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlNoZk%2FdJMcagZe0dj%2F7Z5ikDfGr9gK4Vd0SKtdf1%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;504&quot; height=&quot;404&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://developer.ibm.com/articles/j-zerocopy/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 성능의 핵심은 배치, 공통 메시지 포맷, 압축 쪽이 더욱 본질적이다. zero-copy의 경우 &lt;b&gt;브로커가 디스크의 데이터를 네트워크로 내보내는 특정 경로에서 CPU 복사 비용을 줄여주는 중요한 최적화이긴 하지만, Kafka 전체 처리량을 성립시키는 근본 설계라기보다 이미 효율적으로 쌓인 데이터를 더 싸게 전달하기 위한 전송 최적화에 가깝다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제의식&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 알았다. 카프카는 빠르다. 세가지 이유가 있다. 그렇다. 이해는 했지만, 어려운 내용이다. 특히 너무나도 OS, 컴퓨터 구조적이다. 외우자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q : 카프카는 왜 빠른가요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A : 카프카가 빠른 이유는 대략 세가지가 있습니다~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 면접은 무섭다. 공식 문서라도 한번 제대로 읽어보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;TLS/SSL libraries operate at the user space (in-kernel SSL_sendfile is currently not supported by Kafka). Due to this restriction, sendfile is not used when SSL is enabled. For enabling SSL configuration, refer to&amp;nbsp;security.protocol and security.inter.broker.protocol&lt;br /&gt;&lt;br /&gt;TLS/SSL 라이브러리는 사용자 공간에서 작동합니다(SSL_sendfile 현재 Kafka는 커널 내 실행을 지원하지 않습니다). 이러한 제약으로 인해 SSL이 활성화된 경우 해당 라이브러리는 사용되지 않습니다. SSL 구성 활성화에 대한 자세한 내용 은 및 을 sendfile 참조하십시오. security.protocol security.inter.broker.protocol&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS/SSL이 활성화된 경우 zero copy가 동작하지 않는다는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1774787679227&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kafka:
  bootstrap-servers: ${kafka.servers:localhost:29092}
  properties:
    security.protocol: SASL_SSL
    sasl.mechanism: AWS_MSK_IAM
    sasl.jaas.config: software.amazon.msk.auth.iam.IAMLoginModule required;
    sasl.client.callback.handler.class: software.amazon.msk.auth.iam.IAMClientCallbackHandler&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS MSK를 사용하며, 해당 설정을 활성화해두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안을 위해 IAM을 통해 kafka에 접근하고, 일반 메세지도 암호화된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;u&gt;&lt;b&gt;하지만 난 zero copy의 혜택을 누리지 못하는 건가?&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;접근&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게나 좋은 zero copy를 사용하지 못한다니..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 내 카프카는 느린 걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/msk/latest/developerguide/msk-encryption.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS MSK의 공식 문서&lt;/a&gt;에 따르면 암호화 설정을 활성화하면 CPU 오버헤드가 증가하고 몇 밀리초의 지연 시간이 발생할 수 있다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 대부분의 사용 사례에서는 이러한 차이가 크게 중요하지 않다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다 하더라도 메서드를 직접 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SslTransportLayer vs PlaintextTransportLayer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/common/network/SslTransportLayer.java#L754&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/common/network/SslTransportLayer.java#L754&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774789393925&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;kafka/clients/src/main/java/org/apache/kafka/common/network/SslTransportLayer.java at trunk &amp;middot; apache/kafka&quot; data-og-description=&quot;Apache Kafka - A distributed event streaming platform - apache/kafka&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/common/network/SslTransportLayer.java#L754&quot; data-og-url=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/common/network/SslTransportLayer.java&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cmu2OD/dJMb86nXwE8/6TnzhZnHnkhRvb76OlHGtK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bPn8Jb/dJMb9efdVaN/JmhHk9by70BRH1QsQfYkf1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/common/network/SslTransportLayer.java#L754&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/common/network/SslTransportLayer.java#L754&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cmu2OD/dJMb86nXwE8/6TnzhZnHnkhRvb76OlHGtK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bPn8Jb/dJMb9efdVaN/JmhHk9by70BRH1QsQfYkf1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;kafka/clients/src/main/java/org/apache/kafka/common/network/SslTransportLayer.java at trunk &amp;middot; apache/kafka&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Apache Kafka - A distributed event streaming platform - apache/kafka&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SslTransportLayer의 내용이다.&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1774789152900&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
    if (state == State.CLOSING)
        throw closingException();
    if (state != State.READY)
        return 0;

    if (!flush(netWriteBuffer))
        return 0;

    long channelSize = fileChannel.size();
    if (position &amp;gt; channelSize)
        return 0;
    int totalBytesToWrite = (int) Math.min(Math.min(count, channelSize - position), Integer.MAX_VALUE);

    if (fileChannelBuffer == null) {
        int transferSize = 32768;
        fileChannelBuffer = ByteBuffer.allocateDirect(transferSize);
        fileChannelBuffer.position(fileChannelBuffer.limit());
    }

    int totalBytesWritten = 0;
    long pos = position;
    try {
        while (totalBytesWritten &amp;lt; totalBytesToWrite) {
            if (!fileChannelBuffer.hasRemaining()) {
                fileChannelBuffer.clear();
                int bytesRemaining = totalBytesToWrite - totalBytesWritten;
                if (bytesRemaining &amp;lt; fileChannelBuffer.limit())
                    fileChannelBuffer.limit(bytesRemaining);
                int bytesRead = fileChannel.read(fileChannelBuffer, pos);
                if (bytesRead &amp;lt;= 0)
                    break;
                fileChannelBuffer.flip();
            }
            int networkBytesWritten = write(fileChannelBuffer);
            totalBytesWritten += networkBytesWritten;
            if (fileChannelBuffer.hasRemaining())
                break;
            pos += networkBytesWritten;
        }
        return totalBytesWritten;
    } catch (IOException e) {
        if (totalBytesWritten &amp;gt; 0)
            return totalBytesWritten;
        throw e;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;write(fileChannleBuffer)&lt;/b&gt;을 잘 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1774789332843&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public int write(ByteBuffer src) throws IOException {
    if (state == State.CLOSING)
        throw closingException();
    if (!ready())
        return 0;

    int written = 0;
    while (flush(netWriteBuffer) &amp;amp;&amp;amp; src.hasRemaining()) {
        netWriteBuffer.clear();
        SSLEngineResult wrapResult = sslEngine.wrap(src, netWriteBuffer);
        netWriteBuffer.flip();

        // reject renegotiation if TLS &amp;lt; 1.3, key updates for TLS 1.3 are allowed
        if (wrapResult.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING &amp;amp;&amp;amp;
                wrapResult.getStatus() == Status.OK &amp;amp;&amp;amp;
                !sslEngine.getSession().getProtocol().equals(TLS13)) {
            throw renegotiationException();
        }

        if (wrapResult.getStatus() == Status.OK) {
            written += wrapResult.bytesConsumed();
        } else if (wrapResult.getStatus() == Status.BUFFER_OVERFLOW) {
            // BUFFER_OVERFLOW means that the last `wrap` call had no effect, so we expand the buffer and try again
            netWriteBuffer = Utils.ensureCapacity(netWriteBuffer, netWriteBufferSize());
            netWriteBuffer.position(netWriteBuffer.limit());
        } else if (wrapResult.getStatus() == Status.BUFFER_UNDERFLOW) {
            throw new IllegalStateException(&quot;SSL BUFFER_UNDERFLOW during write&quot;);
        } else if (wrapResult.getStatus() == Status.CLOSED) {
            throw new EOFException();
        }
    }
    return written;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메서드를 보면 알 수 있는 것이 SSL 래핑이 진행된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 보면 알 수 있듯이 Ssl 전송을 진행할 때는 zero copy가 되는 것이 아닌, ssl처리로 인한 로직이 들어간다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PlaintextTransportLayer&lt;/b&gt;의 메서드이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774789507187&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
    return fileChannel.transferTo(position, count, socketChannel);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zero copy를 위한 java.nio의 메서드를 그대로 사용하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 응용계층과 전송계층 사이에서 동작하는 독립적인 SSL/TLS 프로토콜로 인해 zero copy가 동작하지 못하고 별도의 처리가 필요한 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;확인&lt;/h3&gt;
&lt;pre id=&quot;code_1774789663625&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker exec -u 0 -it kafka sh
microdnf install -y strace
sudo docker exec -u 0 -it kafka sh -lc 'id &amp;amp;&amp;amp; ps -o pid,user,comm -p 1 &amp;amp;&amp;amp; strace -fp 1 -e trace=sendfile,sendfile64'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker에 kafka를 동작시킨 뒤 브로커를 plaintext와 ssl 방식으로 모두 동작시켜 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;plaintext로 동작시키면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o99IW/dJMcafe02Ai/j6OOL8YKEQltecIpcEamT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o99IW/dJMcafe02Ai/j6OOL8YKEQltecIpcEamT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o99IW/dJMcafe02Ai/j6OOL8YKEQltecIpcEamT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo99IW%2FdJMcafe02Ai%2Fj6OOL8YKEQltecIpcEamT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;419&quot; height=&quot;437&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zero copy 메서드가 정확히 동작함을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, ssl로 동작시키면 sendfile 시스템콜이 발생하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ef6f53;&quot;&gt;&lt;u&gt;즉, zero copy가 동작하지 않는 것&lt;/u&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;그렇다면 성능은?&lt;/span&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;plaintext가 약간의 성능상 우위를 보이긴 했으나, 필자의 능력이 부족하여 이것이 ssl 처리에 의한 것인지 zero copy에 의한 것인지는 명확하게 정리하지 못했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;plaintext가 성능상 이점이 있는 것은 분명하다. zero copy를 지원하고, 암호화가 적용되지 않기에 가볍다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 암호화가 중요한 우리는 Kafka의 zero copy를 쓸 수 없는 것일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.confluent.io/platform/current/kafka/listeners.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;를 참고한 필자 나름의 결론은 아래와 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Kafka&lt;/b&gt;는 SSL listener와 PLAINTEXT listener를 &lt;u&gt;동시에 운영&lt;/u&gt;할 수 있으며, 운영의 기본 방향은 TLS/SSL 중심으로 가져가되, 내부적으로 충분히 신뢰할 수 있는 네트워크 구간에 한해서는 PLAINTEXT를 선택해 성능 개선을 도모할 수 있다. &lt;br /&gt;다만 이 경우에도 PLAINTEXT는 무분별하게 확장하기보다 내부 전용 listener로 &lt;u&gt;역할을 명확히 분리&lt;/u&gt;하고, 외부 또는 민감한 트래픽은 반드시 SSL/SASL_SSL로 보호하는 방식이 바람직하다. &lt;br /&gt;&lt;u&gt;즉, 실무적으로는 보안을 기본값으로 두되, &lt;u&gt;성능과 운영 효율이 극적으로 중요한 경우&amp;nbsp;&lt;/u&gt;내부 신뢰망에 한해 제한적으로 PLAINTEXT를 활용하는 전략이 현실적인 절충안이라고 볼 수 있다.&lt;/u&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kafka.apache.org/42/design/design/#efficiency&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kafka.apache.org/42/design/design/#efficiency&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/tech/amazon-msk-topic-iam-access-control/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://aws.amazon.com/ko/blogs/tech/amazon-msk-topic-iam-access-control/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.ibm.com/articles/j-zerocopy/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.ibm.com/articles/j-zerocopy/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/msk/latest/developerguide/msk-encryption.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.aws.amazon.com/msk/latest/developerguide/msk-encryption.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/apache/kafka&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/apache/kafka&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.confluent.io/platform/current/kafka/listeners.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.confluent.io/platform/current/kafka/listeners.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/nio/channels/FileChannel.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/nio/channels/FileChannel.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cwiki.apache.org/confluence/display/KAFKA/KIP-317%3A+Add+end-to-end+data+encryption+functionality+to+Apache+Kafka&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://cwiki.apache.org/confluence/display/KAFKA/KIP-317%3A+Add+end-to-end+data+encryption+functionality+to+Apache+Kafka&lt;/a&gt;&lt;/p&gt;</description>
      <category>카프카</category>
      <category>kafka</category>
      <category>SSL</category>
      <category>ssl/tls</category>
      <category>TLS</category>
      <category>zero copy</category>
      <category>카프카</category>
      <category>카프카는 왜 빠를까</category>
      <author>27200</author>
      <guid isPermaLink="true">https://to-travel-coding.tistory.com/477</guid>
      <comments>https://to-travel-coding.tistory.com/477#entry477comment</comments>
      <pubDate>Sun, 29 Mar 2026 22:30:54 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정 요약</title>
      <link>https://to-travel-coding.tistory.com/476</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;길고 긴 개선 과정을 거쳐 최종적으로 마무리한 Kafka Parallel-Consumer의 선정과 결론에 대해 정리하고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 과정이 궁금하신 분께서는 아래 내용을 참고해주시면 감사하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://to-travel-coding.tistory.com/473&quot;&gt;2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(1)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://to-travel-coding.tistory.com/474&quot;&gt;2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(2)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://to-travel-coding.tistory.com/475&quot;&gt;2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(3(완))&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 요구사항을 다시 한번 살펴보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자 1명당 N개의 알림을 받을 수 있다.&lt;/li&gt;
&lt;li&gt;추후 알림 별 시간 설정 기능이 추가될 수 있으나, 현재 모든 알림이 동일한 시간에 제공된다.&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;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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AWS Lambda를 통해 스케줄러를 서비스와 분리하여 이벤트를 발행한다.&lt;/li&gt;
&lt;li&gt;Consumer 서버들이 이를 처리한다.&lt;/li&gt;
&lt;li&gt;Batch 처리를 통해 처리량을 올린다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;485&quot; data-origin-height=&quot;85&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1lQtW/dJMcaaq63sw/lyd19b3XTYM5ZXY0o9QT51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1lQtW/dJMcaaq63sw/lyd19b3XTYM5ZXY0o9QT51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1lQtW/dJMcaaq63sw/lyd19b3XTYM5ZXY0o9QT51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1lQtW%2FdJMcaaq63sw%2Flyd19b3XTYM5ZXY0o9QT51%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;485&quot; height=&quot;85&quot; data-origin-width=&quot;485&quot; data-origin-height=&quot;85&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[GREENROOM KAFKA BATCH BENCHMARK] 
eligible=100000, 
outboxCreated=100000, 
eventPublishSec=0.649, 
totalSec=38.944, 
msgsPerSecond=2567.79&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 작업을 통해 초당 약 2600개의 알림을 전송하고, 이는 기능 요구사항인 &lt;u&gt;&amp;ldquo;1분 내에 10만개의 알림을 전송해야한다.&amp;rdquo;&lt;/u&gt; 를 달성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;하지만 이러한 궁금증이 남았다. 10만 건의 알림이 아니라, 더 많은 알림을 전송해야 한다면 어떻게 해야 할까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 결론을 내렸다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka 토픽의 파티션 수를 늘린다.&lt;/li&gt;
&lt;li&gt;처리할 서버 수를 늘린다.&lt;/li&gt;
&lt;li&gt;서버를 늘릴 수 없다면, 하나의 서버에 더 많은 스레드로 여러 파티션을 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 &lt;span style=&quot;background-color: #f89009;&quot;&gt;파티션의 개수에 종속되는 문제&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;협업 차원에서 카프카 파티션의 개수를 늘리기 위해 인프라팀을 설득해야했고, 이 과정 속에서 발견된 여러 문제점을 찾을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 메시지 순서 보장 문제가 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 파티션 수와 Consumer 수를 늘린다고 성능이 선형 증가하는 것은 아니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;343&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5uYLw/dJMcach9XMV/T3DGacmLEOcaLPGA92trik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5uYLw/dJMcach9XMV/T3DGacmLEOcaLPGA92trik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5uYLw/dJMcach9XMV/T3DGacmLEOcaLPGA92trik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5uYLw%2FdJMcach9XMV%2FT3DGacmLEOcaLPGA92trik%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;706&quot; height=&quot;343&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;343&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 파티션 수가 많아질수록 운영 비용도 증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 리밸런싱 비용이 커질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 파티션 증설은 쉽지만, 되돌리는 것은 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;즉 단순히 &amp;ldquo;더 빠르게 처리하고 싶다&amp;rdquo;는 이유만으로 파티션을 계속 늘리는 것은 적절한 해결책이 아니었다.&lt;/u&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결책 선정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 지점에서 찾은 것이 Kafka Parallel Consumer 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/confluentinc/parallel-consumer&quot;&gt;https://github.com/confluentinc/parallel-consumer&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774165842562&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - confluentinc/parallel-consumer: Parallel Apache Kafka client wrapper with per message ACK, client side queueing, a simp&quot; data-og-description=&quot;Parallel Apache Kafka client wrapper with per message ACK, client side queueing, a simpler consumer/producer API with key concurrency and extendable non-blocking IO processing. - confluentinc/paral...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/confluentinc/parallel-consumer&quot; data-og-url=&quot;https://github.com/confluentinc/parallel-consumer&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/xllQJ/dJMb81GWxY7/3hDbUQ3X44T7kfxa8b1Kb1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/AYUHx/dJMb87f5MkZ/AMMB3TrCOGR5ikvKKLb97k/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/eXe5r/dJMb9gxkI5h/squX6EKIMZas8DqfBfHkEk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=144_162_216_240&quot;&gt;&lt;a href=&quot;https://github.com/confluentinc/parallel-consumer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/confluentinc/parallel-consumer&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/xllQJ/dJMb81GWxY7/3hDbUQ3X44T7kfxa8b1Kb1/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/AYUHx/dJMb87f5MkZ/AMMB3TrCOGR5ikvKKLb97k/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/eXe5r/dJMb9gxkI5h/squX6EKIMZas8DqfBfHkEk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=144_162_216_240');&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;GitHub - confluentinc/parallel-consumer: Parallel Apache Kafka client wrapper with per message ACK, client side queueing, a simp&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Parallel Apache Kafka client wrapper with per message ACK, client side queueing, a simpler consumer/producer API with key concurrency and extendable non-blocking IO processing. - confluentinc/paral...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Confluent의 &lt;b&gt;Parallel Consumer&lt;/b&gt;는 &lt;b&gt;하나의 Kafka Consumer 인스턴스 안에서 여러 레코드를 동시에 처리&lt;/b&gt;할 수 있게 해 주는 래퍼 라이브러리다. 이 라이브러리를 통해 &lt;b&gt;토픽 파티션 수를 늘리지 않고도 consumer parallelism을 높일 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 시스템을 비교하자면 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;기존 Kafka Consumer&lt;/th&gt;
&lt;th&gt;Kafka Parallel Consumer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;병렬성 기준&lt;/td&gt;
&lt;td&gt;기본적으로 파티션 단위&lt;/td&gt;
&lt;td&gt;&lt;b&gt;단일 consumer 내부에서도&lt;/b&gt; 동시 처리 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;병렬성 확장 방법&lt;/td&gt;
&lt;td&gt;컨슈머 수 증가 또는 파티션 수 증가&lt;/td&gt;
&lt;td&gt;&lt;code&gt;maxConcurrency&lt;/code&gt; 조절로 확장 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파티션 의존도&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;상대적으로 낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동일 파티션 내부 처리&lt;/td&gt;
&lt;td&gt;보통 순차 처리&lt;/td&gt;
&lt;td&gt;여러 레코드 동시 처리 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;순서 보장 옵션&lt;/td&gt;
&lt;td&gt;기본적으로 파티션 순서 중심&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UNORDERED&lt;/code&gt; / &lt;code&gt;PARTITION&lt;/code&gt; / &lt;code&gt;KEY&lt;/code&gt; 선택 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;offset commit 관리&lt;/td&gt;
&lt;td&gt;병렬 처리 시 직접 관리 복잡&lt;/td&gt;
&lt;td&gt;라이브러리가 commit 처리 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;적합한 상황&lt;/td&gt;
&lt;td&gt;단순 구조, 파티션 기반 확장이 충분할 때&lt;/td&gt;
&lt;td&gt;파티션 수를 늘리지 않고 처리량을 높이고 싶을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장점&lt;/td&gt;
&lt;td&gt;단순하고 표준적&lt;/td&gt;
&lt;td&gt;높은 동시성, ordering 전략 선택 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주의점&lt;/td&gt;
&lt;td&gt;파티션 수가 병렬성 상한&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UNORDERED&lt;/code&gt;는 순서 민감한 서비스에 부적합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 라이브러리를 해결책으로 검토한 이유는 두 가지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 현재 구조에서는 파티션 수를 무작정 늘리기보다 &lt;b&gt;기존 토픽 구조를 유지하면서 처리량을 높이는 것&lt;/b&gt;이 더 적절했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;705&quot; data-origin-height=&quot;743&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMtsbq/dJMcaflEms4/pkY2WlZHMQprkidZlh4wc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMtsbq/dJMcaflEms4/pkY2WlZHMQprkidZlh4wc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMtsbq/dJMcaflEms4/pkY2WlZHMQprkidZlh4wc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMtsbq%2FdJMcaflEms4%2FpkY2WlZHMQprkidZlh4wc0%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;705&quot; height=&quot;743&quot; data-origin-width=&quot;705&quot; data-origin-height=&quot;743&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 일반 Kafka Consumer에서 애플리케이션 레벨로 스레드 풀을 붙여 병렬 처리하려면 &lt;b&gt;offset commit과 ordering 보장, retry 처리&lt;/b&gt;를 직접 관리해야 해서 구현 복잡도가 높다. Parallel Consumer는 이 부분을 감싸 주고, 개발자는 처리 함수에 더 집중할 수 있도록 설계되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;무엇보다 기획 요구사항은 &amp;ldquo;순차 처리&amp;rdquo; 보다는 &amp;ldquo;시간 내 도착&amp;rdquo;이 더욱 중요한 문제였다.&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka가 기본적으로 보장하는 건:&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;같은 파티션에 들어간 레코드들은 오프셋 순서대로 읽힌다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;컨슈머가 &lt;b&gt;그 순서대로 처리할지&lt;/b&gt;는 애플리케이션이 어떻게 구현하느냐에 달려있다(특히 병렬 처리하면 깨지기 쉬움)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Kafka는 &amp;ldquo;읽히는 순서&amp;rdquo;는 주지만, 병렬 처리에서 &amp;ldquo;처리 완료 순서&amp;rdquo;까지 자동으로 맞춰주진 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI649N/dJMcagdNGjl/5TLZ6xKPHB9FhPPpXz8w8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI649N/dJMcagdNGjl/5TLZ6xKPHB9FhPPpXz8w8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI649N/dJMcagdNGjl/5TLZ6xKPHB9FhPPpXz8w8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI649N%2FdJMcagdNGjl%2F5TLZ6xKPHB9FhPPpXz8w8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;708&quot; height=&quot;338&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 11을 커밋하는 순간 10도 처리된 것으로 간주될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parallel-Consumer은 이런 요구를 겨냥해 순서 보장 범위를 &lt;b&gt;파티션 전체 &amp;rarr; 키 단위 &amp;rarr; 무순서&lt;/b&gt;로 단계적으로 제시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중에서도 순서가 필요없고, 처리량이 더욱 중요한 서비스 특성상 무순서 방식을 고려했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5uzY6/dJMcaaSclm4/4yKBJY39l5QsJk5wlpA6OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5uzY6/dJMcaaSclm4/4yKBJY39l5QsJk5wlpA6OK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5uzY6/dJMcaaSclm4/4yKBJY39l5QsJk5wlpA6OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5uzY6%2FdJMcaaSclm4%2F4yKBJY39l5QsJk5wlpA6OK%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;703&quot; height=&quot;272&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서를 전혀 신경쓰지 않고 처리 가능한 쓰레드가 맡아 처리하는 방식으로 작업이 진행된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;도입&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1496&quot; data-origin-height=&quot;1302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDMn5K/dJMcajas7tg/2xMIu5hAkC3myl8sKtjaIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDMn5K/dJMcajas7tg/2xMIu5hAkC3myl8sKtjaIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDMn5K/dJMcajas7tg/2xMIu5hAkC3myl8sKtjaIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDMn5K%2FdJMcajas7tg%2F2xMIu5hAkC3myl8sKtjaIK%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;1496&quot; height=&quot;1302&quot; data-origin-width=&quot;1496&quot; data-origin-height=&quot;1302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.ordering(ParallelConsumerOptions.ProcessingOrder.UNORDERED)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 처리 순서 정책을 정하는 옵션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;UNORDERED&lt;/code&gt;는 순서를 보장하지 않는 대신 가장 높은 동시성을 확보할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 요구사항을 완벽히 충족시키는 정책이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.maxConCurrency(10)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시 처리 작업 수의 상한을 정하는 옵션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 개수와 별개로 최대 10개의 레코드가 병렬로 처리될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무조건 높다고 좋은 것은 아니기에 여러가지 상황에 대한 테스트 진행 후 10개로 변경하였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.commitMode(ParallelConsumerOptions.CommitMode.PERIODIC_CONSUMER_ASYNCHRONOUS)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset commit 방식을 정하는 옵션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 설정은 offset을 주기적으로 비동기로 커밋하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 호출이 너무 자주 발생하는 상황을 방지하기 위해 설정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 비동기 커밋 특성 상 일부 메시지에 대한 재처리가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 알림을 db에서 조회해가는 서비스 특성을 이용하여 재처리가 발생하더라도 사용자가 느낄 수 없도록 처리했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h2&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;&lt;b&gt;초당 처리량 362.12건 &amp;rarr; 6,767.27건&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;약 18.7배 성능 향상&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;99&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0DEmn/dJMcafsoUc5/dDcgnLx3zgErdz6EbYuxF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0DEmn/dJMcafsoUc5/dDcgnLx3zgErdz6EbYuxF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0DEmn/dJMcafsoUc5/dDcgnLx3zgErdz6EbYuxF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0DEmn%2FdJMcafsoUc5%2FdDcgnLx3zgErdz6EbYuxF1%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;597&quot; height=&quot;99&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;99&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 5 Thread
[GREENROOM KAFKA PARALLEL-CONSUMER BATCH BENCHMARK] 
eligible=100000, 
outboxCreated=100000, 
eventPublishSec=1.277, 
totalSec=19.868, 
msgsPerSecond=5033.22
thread=pc-pool-4-thread-1, accepted=20938
thread=pc-pool-4-thread-2, accepted=20237
thread=pc-pool-4-thread-3, accepted=19929
thread=pc-pool-4-thread-4, accepted=20102
thread=pc-pool-4-thread-5, accepted=18794

# 10 Thread
[GREENROOM KAFKA PARALLEL-CONSUMER BATCH 10 BENCHMARK] 
eligible=100000, 
outboxCreated=100000, 
eventPublishSec=1.297, 
totalSec=14.777, 
msgsPerSecond=6767.27
thread=pc-pool-4-thread-1, accepted=20938
thread=pc-pool-4-thread-2, accepted=20237
thread=pc-pool-4-thread-3, accepted=19929
thread=pc-pool-4-thread-4, accepted=20102
thread=pc-pool-4-thread-5, accepted=18794
thread=pc-pool-7-thread-1, accepted=10246
thread=pc-pool-7-thread-10, accepted=9936
thread=pc-pool-7-thread-2, accepted=10413
thread=pc-pool-7-thread-3, accepted=10276
thread=pc-pool-7-thread-4, accepted=10161
thread=pc-pool-7-thread-5, accepted=9780
thread=pc-pool-7-thread-6, accepted=9872
thread=pc-pool-7-thread-7, accepted=10015
thread=pc-pool-7-thread-8, accepted=8721
thread=pc-pool-7-thread-9, accepted=10580&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 선정한 10개의 쓰레드 방식은 초당 6767개의 알림을 처리하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 최초 구현인 362건 대비 18.7배 상승한 결과이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;437&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIOQ3n/dJMcabQ4uGN/ih7EvA3gIoqsNxbTxKTb11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIOQ3n/dJMcabQ4uGN/ih7EvA3gIoqsNxbTxKTb11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIOQ3n/dJMcabQ4uGN/ih7EvA3gIoqsNxbTxKTb11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIOQ3n%2FdJMcabQ4uGN%2Fih7EvA3gIoqsNxbTxKTb11%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;853&quot; height=&quot;437&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;437&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnN8WW/dJMcaaLpyRU/Cl9JiCcnOKzXDPAxLpDgP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnN8WW/dJMcaaLpyRU/Cl9JiCcnOKzXDPAxLpDgP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnN8WW/dJMcaaLpyRU/Cl9JiCcnOKzXDPAxLpDgP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnN8WW%2FdJMcaaLpyRU%2FCl9JiCcnOKzXDPAxLpDgP1%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;860&quot; height=&quot;428&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;최종 시퀀스 다이어그램&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;837&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbYxnG/dJMcajuMNiz/TIBlPHYHqdA4T8TlNOH8E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbYxnG/dJMcajuMNiz/TIBlPHYHqdA4T8TlNOH8E0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbYxnG/dJMcajuMNiz/TIBlPHYHqdA4T8TlNOH8E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbYxnG%2FdJMcajuMNiz%2FTIBlPHYHqdA4T8TlNOH8E0%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;1010&quot; height=&quot;837&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;837&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&amp;nbsp;:&lt;br /&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/7181840&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://d2.naver.com/helloworld/7181840&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=UhnERp2AYRo&amp;amp;t=1507s&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=UhnERp2AYRo&amp;amp;t=1507s&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/confluentinc/parallel-consumer&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/confluentinc/parallel-consumer&lt;/a&gt;&lt;/p&gt;</description>
      <category>카프카</category>
      <category>consumer</category>
      <category>kafka</category>
      <category>parallel</category>
      <category>성능 개선</category>
      <category>알림</category>
      <category>카프카</category>
      <author>27200</author>
      <guid isPermaLink="true">https://to-travel-coding.tistory.com/476</guid>
      <comments>https://to-travel-coding.tistory.com/476#entry476comment</comments>
      <pubDate>Sun, 22 Mar 2026 20:14:21 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(3(완))</title>
      <link>https://to-travel-coding.tistory.com/475</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;‼️  최종 개선 결과  ‼️&lt;br /&gt;&lt;br /&gt;- 초당 처리량 362.12건 &amp;rarr; 6,767.27건&lt;br /&gt;- 약 18.7배 성능 향상&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size18&quot;&gt;총 3편의 내용으로 구성된 개선 과정이다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편 : &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://to-travel-coding.tistory.com/473&quot;&gt;2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(1)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774097448372&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;[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(1)&quot; data-og-description=&quot;‼️  최종 개선 결과  ‼️- 초당 처리량 362.12건 &amp;rarr; 6,767.27건- 약 18.7배 성능 향상개요프로젝트를 진행하는 과정에서 알림 기능을 추가해야 했다.기능 요구조건은 아래와 같았다.사용자 1명&quot; data-og-host=&quot;to-travel-coding.tistory.com&quot; data-og-source-url=&quot;https://to-travel-coding.tistory.com/473&quot; data-og-url=&quot;https://to-travel-coding.tistory.com/473&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/EpXyc/dJMb9lk6pvg/c58QL0CyExQ72ANXHRULKk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cR8vH4/dJMb9lk6pvf/qIk7k2HMwLaYqFQNvS0xcK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cocEM4/dJMb9efddbe/bwtem9bECzxoMRRYVlozeK/img.jpg?width=2048&amp;amp;height=2048&amp;amp;face=0_0_2048_2048&quot;&gt;&lt;a href=&quot;https://to-travel-coding.tistory.com/473&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://to-travel-coding.tistory.com/473&quot;&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;‼️  최종 개선 결과  ‼️- 초당 처리량 362.12건 &amp;rarr; 6,767.27건- 약 18.7배 성능 향상개요프로젝트를 진행하는 과정에서 알림 기능을 추가해야 했다.기능 요구조건은 아래와 같았다.사용자 1명&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;to-travel-coding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2편 : &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://to-travel-coding.tistory.com/474&quot;&gt;2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(2)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774097459132&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;[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(2)&quot; data-og-description=&quot;‼️  최종 개선 결과  ‼️- 초당 처리량 362.12건 &amp;rarr; 6,767.27건- 약 18.7배 성능 향상총 3편의 내용이다.3차 구현조회는 단일 서버, 처리는 Kafka Consumer 분산 처리 2차 구현에서는 target 단위 Redis &quot; data-og-host=&quot;to-travel-coding.tistory.com&quot; data-og-source-url=&quot;https://to-travel-coding.tistory.com/474&quot; data-og-url=&quot;https://to-travel-coding.tistory.com/474&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cxZTVg/dJMb8XkeCwA/fYOSQoxF32v8j6RIzI9F5k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cMc1Qb/dJMb8WMoQtT/TXEAukmdIirV5GaIxuYcLk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/MKXth/dJMb8PGvp1v/7bVEgA9CYBsfkvgKsxre7k/img.jpg?width=2048&amp;amp;height=2048&amp;amp;face=0_0_2048_2048&quot;&gt;&lt;a href=&quot;https://to-travel-coding.tistory.com/474&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://to-travel-coding.tistory.com/474&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cxZTVg/dJMb8XkeCwA/fYOSQoxF32v8j6RIzI9F5k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cMc1Qb/dJMb8WMoQtT/TXEAukmdIirV5GaIxuYcLk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/MKXth/dJMb8PGvp1v/7bVEgA9CYBsfkvgKsxre7k/img.jpg?width=2048&amp;amp;height=2048&amp;amp;face=0_0_2048_2048');&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;[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(2)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;‼️  최종 개선 결과  ‼️- 초당 처리량 362.12건 &amp;rarr; 6,767.27건- 약 18.7배 성능 향상총 3편의 내용이다.3차 구현조회는 단일 서버, 처리는 Kafka Consumer 분산 처리 2차 구현에서는 target 단위 Redis&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;to-travel-coding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5차 구현(최종)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka를 도입한 이후, 조회는 단일 서버에서 수행하고 실제 처리는 여러 consumer가 분산해서 처리하는 구조까지는 만들 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 구조에도 위에서 살펴본 것 같은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f89009;&quot;&gt;한계&lt;/span&gt;가 남아 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;partition 수가 곧 병렬성의 상한이 된다.&lt;/li&gt;
&lt;li&gt;partition을 늘리지 않으면 같은 partition 내부에서는 기본적으로 순차 처리된다.&lt;/li&gt;
&lt;li&gt;partition을 무작정 늘리는 것은 ordering, 운영 복잡도, 리밸런싱 비용, 브로커 부담 등 추가적인 문제를 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 단순히 &amp;ldquo;더 빠르게 처리하고 싶다&amp;rdquo;는 이유만으로 파티션을 계속 늘리는 것은 적절한 해결책이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 지점에서 도입한 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;Kafka Parallel Consumer&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;도입 이유&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;1. 메시지 순서 보장 문제와 무관하다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 Kafka consumer 모델에서는 보통 partition 단위로 병렬성이 결정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, partition이 n개라면 consumer group 안에서 의미 있는 병렬 처리도 사실상 n개 흐름에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 목표 구현점은 아래와 같았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토픽 partition을 함부로 조절하고 싶지 않다.&lt;/li&gt;
&lt;li&gt;알림 처리를 일정한 순서로 제공해야 하는 기능 요구 사항이 없다.&lt;/li&gt;
&lt;li&gt;그럼에도 처리 자체는 더 병렬적으로 수행하고 싶다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Parallel Consumer가 이런 요구사항에 맞았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 라이브러리는 poll은 consumer가 하되, 실제 record 처리는 내부적으로 더 병렬화할 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;partition 수를 늘리지 않고도 처리 동시성을 높일 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;2. partition을 늘릴 때 생기는 ordering/운영 부담을 줄이고 싶었다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 확인했듯이 partition 증설은 단순한 설정 변경이 아니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key ordering 보장이 달라질 수 있고&lt;/li&gt;
&lt;li&gt;consumer rebalance 비용이 생기며&lt;/li&gt;
&lt;li&gt;broker 메타데이터 및 운영 비용도 커질 수 있다.&lt;/li&gt;
&lt;li&gt;Apache Kafka 공식 문서:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://kafka.apache.org/documentation/&quot;&gt;Ordering Guarantees&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;반면 Kafka Parallel Consumer는 같은 partition을 유지한 채,&lt;/span&gt;&lt;/u&gt;&lt;br /&gt;&lt;u&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;처리 로직만 더 병렬적으로 실행할 수 있도록 도와준다.&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;특히 이번 구현에서는 기농 요구 사항에 맞춰&lt;span&gt;&amp;nbsp;&lt;/span&gt;UNORDERED&lt;span&gt;&amp;nbsp;&lt;/span&gt;모드를 사용해,&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;순서 보장보다 처리량을 극대화하는 방식으로 병렬성을 확보했다.&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 5차 구현은 &amp;ldquo;파티션을 늘리기 전에, 현재 partition 구조 안에서 처리량을 더 높여보자&amp;rdquo;는 시도였다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;3. 단순 worker pool보다 offset commit을 더 안전하게 다루고 싶었다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 poll thread와 worker thread를 분리해서 구현하는 것도 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 이전 단계에서는 수동 ack와 worker pool을 이용해 비슷한 실험을 했다.&lt;/p&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;poll과 워커 스레드를 분리해야 한다.&lt;/li&gt;
&lt;li&gt;어떤 offset까지 안전하게 commit 가능한지 직접 고려해야 한다.&lt;/li&gt;
&lt;li&gt;선행 메시지가 끝나지 않았는데 후행 메시지가 끝난 경우 commit 전략이 복잡해진다.&lt;/li&gt;
&lt;li&gt;구현 실수 시 메시지 유실이나 중복 처리 위험이 커진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Parallel Consumer는 이런 부분을 라이브러리 차원에서 추상화해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 5차 구현에서 이 라이브러리를 도입한 이유는,&lt;br /&gt;단순히 &amp;ldquo;병렬로 처리하고 싶어서&amp;rdquo;가 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;partition 내부 병렬 처리와 offset commit 관리를 더 안전하고 일관되게 가져가기 위해서&lt;/b&gt;&lt;/span&gt;였다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; 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-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1793&quot; data-origin-height=&quot;1539&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tyEGM/dJMcaf0dLIU/1vCwpnxTrp0vEB9c3lSGkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tyEGM/dJMcaf0dLIU/1vCwpnxTrp0vEB9c3lSGkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tyEGM/dJMcaf0dLIU/1vCwpnxTrp0vEB9c3lSGkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtyEGM%2FdJMcaf0dLIU%2F1vCwpnxTrp0vEB9c3lSGkk%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;1793&quot; height=&quot;1539&quot; data-origin-width=&quot;1793&quot; data-origin-height=&quot;1539&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;haxe&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 구현 예시
@Service
@RequiredArgsConstructor
public class GreenroomNotificationKafkaParallelConsumerRunner {

    private static final String GROUP_ID = &quot;greenroom-notification-parallel-consumer-group-v1&quot;;

    private final KafkaProperties kafkaProperties;
    private final ObjectMapper objectMapper;
    private final GreenroomNotificationKafkaBenchmarkConsumerService consumerService;

    public RunningConsumer start() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;(kafkaProperties.buildConsumerProperties());
        props.put(ConsumerConfig.GROUP_ID_CONFIG, GROUP_ID);
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;latest&quot;);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

        Consumer&amp;lt;String, String&amp;gt; consumer = new KafkaConsumer&amp;lt;&amp;gt;(props);

        var options = ParallelConsumerOptions.&amp;lt;String, String&amp;gt;builder()
            .consumer(consumer)
            .ordering(ParallelConsumerOptions.ProcessingOrder.UNORDERED)
            .maxConcurrency(10)
            .commitMode(ParallelConsumerOptions.CommitMode.PERIODIC_CONSUMER_ASYNCHRONOUS)
            .ignoreReflectiveAccessExceptionsForAutoCommitDisabledCheck(true)
            .build();

        ParallelStreamProcessor&amp;lt;String, String&amp;gt; processor =
            ParallelStreamProcessor.createEosStreamProcessor(options);

        processor.subscribe(singleton(GreenroomNotificationKafkaParallelConsumerBenchmarkService.TOPIC));

        ExecutorService pollExecutor = Executors.newSingleThreadExecutor(runnable -&amp;gt; {
            Thread thread = new Thread(runnable);
            thread.setName(&quot;kafka-parallel-consumer-poller&quot;);
            return thread;
        });

        Future&amp;lt;?&amp;gt; pollFuture = pollExecutor.submit(() -&amp;gt; processor.poll(context -&amp;gt; {
            try {
                consumerService.consume(
                    objectMapper.readValue(
                        context.getSingleConsumerRecord().value(),
                        GreenroomNotificationOutboxSaveEvent.class
                    )
                );
            } catch (Exception exception) {
                throw new IllegalStateException(&quot;Parallel consumer processing failed&quot;, exception);
            }
        }));

        return new RunningConsumer(processor, pollExecutor, pollFuture);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000;&quot; 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-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;99&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u4RJd/dJMcacPY88G/wAYSQVetO5n8tsMWH3RuK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u4RJd/dJMcacPY88G/wAYSQVetO5n8tsMWH3RuK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u4RJd/dJMcacPY88G/wAYSQVetO5n8tsMWH3RuK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu4RJd%2FdJMcacPY88G%2FwAYSQVetO5n8tsMWH3RuK1%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;597&quot; height=&quot;99&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;99&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# 5 Thread
[GREENROOM KAFKA PARALLEL-CONSUMER BATCH BENCHMARK] 
eligible=100000, 
outboxCreated=100000, 
eventPublishSec=1.277, 
totalSec=19.868, 
msgsPerSecond=5033.22
thread=pc-pool-4-thread-1, accepted=20938
thread=pc-pool-4-thread-2, accepted=20237
thread=pc-pool-4-thread-3, accepted=19929
thread=pc-pool-4-thread-4, accepted=20102
thread=pc-pool-4-thread-5, accepted=18794

# 10 Thread
[GREENROOM KAFKA PARALLEL-CONSUMER BATCH 10 BENCHMARK] 
eligible=100000, 
outboxCreated=100000, 
eventPublishSec=1.297, 
totalSec=14.777, 
msgsPerSecond=6767.27
thread=pc-pool-4-thread-1, accepted=20938
thread=pc-pool-4-thread-2, accepted=20237
thread=pc-pool-4-thread-3, accepted=19929
thread=pc-pool-4-thread-4, accepted=20102
thread=pc-pool-4-thread-5, accepted=18794
thread=pc-pool-7-thread-1, accepted=10246
thread=pc-pool-7-thread-10, accepted=9936
thread=pc-pool-7-thread-2, accepted=10413
thread=pc-pool-7-thread-3, accepted=10276
thread=pc-pool-7-thread-4, accepted=10161
thread=pc-pool-7-thread-5, accepted=9780
thread=pc-pool-7-thread-6, accepted=9872
thread=pc-pool-7-thread-7, accepted=10015
thread=pc-pool-7-thread-8, accepted=8721
thread=pc-pool-7-thread-9, accepted=10580&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;테스트에서는 쓰레드가 10개인 경우가 더 속도가 빨랐지만, 이는 실제 서버에 다양한 상황에 대한 테스트를 진행한 후 적합한 값을 찾아서 적용하면 좋을 것 같다. 필자의 경우 10개로 적용했다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5차 구현의 한계와 문제점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5차 구현에서는 Kafka&lt;span&gt;&amp;nbsp;&lt;/span&gt;parallel-consumer를 도입해, partition 수를 늘리지 않고도 같은 partition 내부 처리 병렬성을 높이려 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 단계보다 더 높은 처리량을 기대할 수 있었고, 직접 worker pool과 manual ack를 관리하는 것보다 구현 부담도 줄일 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 방식도 완전한 해결책은 아니며, 여전히 여러 한계가 존재한다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;1. 병목이 Kafka가 아니라 DB일 수 있다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parallel-consumer를 도입하면 Kafka 소비 병렬성은 높일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 처리 로직이 다음과 같이 DB 작업 중심이라면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;outbox insert&lt;/li&gt;
&lt;li&gt;target select&lt;/li&gt;
&lt;li&gt;target update&lt;/li&gt;
&lt;li&gt;flush&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 병목은 Kafka가 아니라 DB가 될 가능성이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;span&gt;&amp;nbsp;&lt;/span&gt;maxConcurrency를 높여도 DB가 감당하지 못하면 기대만큼 성능이 오르지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 병렬성이 높아질수록 DB write 경쟁, flush 비용, connection 사용량이 더 커질 수 있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;UNORDERED는 순서 보장을 포기하는 방식이다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5차 구현에서는 처리량을 우선하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;UNORDERED&lt;span&gt;&amp;nbsp;&lt;/span&gt;모드를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 가장 높은 동시성을 제공하지만, 반대로 순서 보장은 하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 다음과 같은 상황에서는 주의가 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 사용자에 대한 알림 순서가 중요한 경우&lt;/li&gt;
&lt;li&gt;같은 ticket에 대한 sequence 처리 순서가 중요한 경우&lt;/li&gt;
&lt;li&gt;앞선 이벤트보다 뒤 이벤트가 먼저 반영되면 안 되는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 실험에서는 처리량 측정 목적상 괜찮았지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영 환경에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;순서 보장이 필요한지 여부를 먼저 판단해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;3. partition 수를 늘리지 않는 대신, 내부 처리 복잡도가 증가한다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5차 구현의 장점은 partition 수를 늘리지 않고도 병렬성을 확보하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그 대가로 consumer 처리 모델 자체가 더 복잡해진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 Kafka consumer보다 이해 비용이 크다&lt;/li&gt;
&lt;li&gt;디버깅 포인트가 늘어난다&lt;/li&gt;
&lt;li&gt;실패/재처리/offset commit 동작을 라이브러리 동작까지 이해해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 단순 consumer group 구조보다 운영과 디버깅 난이도가 올라갈 수 있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;4. 라이브러리 의존성이 생긴다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5차 구현은 Kafka 기본 기능만으로 만든 구조가 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parallel-consumer라는 외부 라이브러리에 의존한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말은 곧:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라이브러리 버전 호환성 문제&lt;/li&gt;
&lt;li&gt;Spring Kafka / Kafka Client / Java 버전과의 충돌 가능성&lt;/li&gt;
&lt;li&gt;장애 발생 시 공식 Kafka 문서만으로는 해결되지 않는 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등을 함께 감수해야 한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 구현 중에도&lt;span&gt;&amp;nbsp;&lt;/span&gt;enable.auto.commit&lt;span&gt;&amp;nbsp;&lt;/span&gt;reflection 검사 문제처럼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 Kafka consumer만 쓸 때보다 추가적인 호환성 이슈가 발생할 수 있었다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; 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;5차 구현은 분명 의미 있는 개선이었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;partition 수를 늘리지 않고 병렬성을 높일 수 있었다&lt;/li&gt;
&lt;li&gt;manual ack + worker pool보다 구조가 정리되었다&lt;/li&gt;
&lt;li&gt;offset 관리 부담을 일부 라이브러리에 위임할 수 있었다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여전히 다음 문제는 남아 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 병목 가능성&lt;/li&gt;
&lt;li&gt;순서 보장 포기&lt;/li&gt;
&lt;li&gt;운영 복잡도 증가&lt;/li&gt;
&lt;li&gt;외부 라이브러리 의존성&lt;/li&gt;
&lt;li&gt;조회/발행 비용은 별도&lt;/li&gt;
&lt;li&gt;더 큰 규모에서는 여전히 partition 전략 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 5차 구현은 최종 종착점이라기보다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;기존 Kafka 기반 구조 위에서 partition 내부 병렬성을 높이기 위한 현실적인 개선 단계&lt;/b&gt;라&lt;/span&gt;고 보는 것이 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1차 ~ 5차 구현 흐름 정리&lt;/b&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1차 구현: 스케줄러가 직접 조회하고 직접 처리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 처음에는 스케줄러가 직접 due target을 조회하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 target에 대해 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;sendNotification&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는 DB 저장을 수행한 뒤, target 상태를 갱신하는 구조였다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조가 단순함&lt;/li&gt;
&lt;li&gt;구현이 빠름&lt;/li&gt;
&lt;li&gt;모든 로직이 한 메서드 안에 들어감&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;문제점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 트랜잭션에 너무 많은 작업이 묶임&lt;/li&gt;
&lt;li&gt;배치/청크 처리 없음&lt;/li&gt;
&lt;li&gt;실패 처리 부족&lt;/li&gt;
&lt;li&gt;서버가 여러 대면 중복 처리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2차 구현: target별 Redis 분산 락 적용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 알림을 막기 위해 target 단위 Redis 락을 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 target마다 개별 lock key를 잡고, lock 획득에 성공한 서버만 해당 target을 처리하도록 만들었다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 target의 중복 처리는 줄일 수 있음&lt;/li&gt;
&lt;li&gt;서로 다른 target은 병렬 처리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;문제점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 서버가 여전히 같은 시점에 DB를 조회함&lt;/li&gt;
&lt;li&gt;조회 부하가 서버 수만큼 반복될 수 있음&lt;/li&gt;
&lt;li&gt;Redis lock 시도도 target 수만큼 발생함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 처리 중복은 줄었지만, 조회 비용 최적화는 해결하지 못했다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3차 구현: 조회는 단일 서버, 처리는 Kafka consumer group 분산 처리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 조회 단계와 처리 단계를 분리했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회는 전역 Redis 락으로 한 서버만 수행&lt;/li&gt;
&lt;li&gt;조회된 target은 Kafka 이벤트로 발행&lt;/li&gt;
&lt;li&gt;실제 처리는 여러 consumer가 분산 소비&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회 중복 제거&lt;/li&gt;
&lt;li&gt;consumer group 기반 분산 처리 가능&lt;/li&gt;
&lt;li&gt;서버 확장에 따라 처리량 확장 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;문제점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka 도입으로 구조 복잡도 증가&lt;/li&gt;
&lt;li&gt;메시지 중복/재처리/idempotency 고려 필요&lt;/li&gt;
&lt;li&gt;여전히 partition 수가 병렬성 상한이 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4차 구현: Kafka consumer 처리 최적화, 배치 저장 도입&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka consumer가 메시지를 하나 받을 때마다 DB에 바로 저장하면 DB 부하가 크기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer 내부에서 메시지를 모아 200건 단위로 배치 저장하는 방식을 적용했다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지마다 DB write하지 않음&lt;/li&gt;
&lt;li&gt;saveAll&lt;span&gt;&amp;nbsp;&lt;/span&gt;기반 batch insert/update 가능&lt;/li&gt;
&lt;li&gt;idle flush로 마지막 남은 건도 저장 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;문제점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 버퍼 관리 필요&lt;/li&gt;
&lt;li&gt;flush 전 장애 시 유실 가능성 고려 필요&lt;/li&gt;
&lt;li&gt;배치 크기와 idle 시간 튜닝 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 4차 구현은 Kafka 구조 위에서 DB write 비용을 줄이기 위한 최적화 단계였다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5차 구현: Kafka Parallel Consumer 도입&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 partition 수를 늘리지 않고도 더 높은 병렬성을 확보하기 위해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka&lt;span&gt;&amp;nbsp;&lt;/span&gt;parallel-consumer를 도입했다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 partition 내부에서도 병렬 처리 가능&lt;/li&gt;
&lt;li&gt;UNORDERED&lt;span&gt;&amp;nbsp;&lt;/span&gt;+&lt;span&gt;&amp;nbsp;&lt;/span&gt;maxConcurrency로 처리량 개선 시도&lt;/li&gt;
&lt;li&gt;manual ack + worker pool보다 offset 관리가 정리됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;문제점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순서 보장 포기&lt;/li&gt;
&lt;li&gt;DB가 병목이면 성능 향상이 제한적&lt;/li&gt;
&lt;li&gt;라이브러리 의존성 증가&lt;/li&gt;
&lt;li&gt;운영/디버깅 복잡도 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;최종 흐름 요약&lt;/b&gt;&lt;/h2&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;1차:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;단순 직접 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2차:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;target별 락으로 중복 처리 방지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;3차:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;조회와 처리를 분리하고 Kafka 도입&lt;/li&gt;
&lt;li&gt;&lt;b&gt;4차:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Kafka consumer의 DB 저장을 배치화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;5차:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;partition 내부 병렬성을 높이기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;parallel-consumer&lt;span&gt;&amp;nbsp;&lt;/span&gt;도입&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 전체 개선 흐름은 다음 방향으로 진화했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;단순 구현&lt;/li&gt;
&lt;li&gt;중복 처리 방지&lt;/li&gt;
&lt;li&gt;조회/처리 분리&lt;/li&gt;
&lt;li&gt;DB 쓰기 최적화&lt;/li&gt;
&lt;li&gt;Kafka 소비 병렬성 최적화&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 통해 단순 스케줄러 기반 구현에서 시작해,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점차&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;분산 처리, 메시지 기반 아키텍처, 배치 처리, 병렬 consumer 최적화&lt;/b&gt;까지 확장해 나간 구조라고 정리할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;번외&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;837&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf3nxG/dJMcaaxSqX2/1dfbay7xjdILIstkEQl3LK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf3nxG/dJMcaaxSqX2/1dfbay7xjdILIstkEQl3LK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf3nxG/dJMcaaxSqX2/1dfbay7xjdILIstkEQl3LK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf3nxG%2FdJMcaaxSqX2%2F1dfbay7xjdILIstkEQl3LK%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;1010&quot; height=&quot;837&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;837&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;조회 및 이벤트 발행 로직을 Spring Scheduler가 아닌 AWS Lambda로 분리했다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Spring 서버 내부 스케줄러가 주기적으로 실행되면서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 대상 조회와 이벤트 발행까지 모두 담당하는 구조를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이후 이 역할을 애플리케이션 서버에서 완전히 분리하여,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AWS Lambda가 주기적으로 실행되며 조회 후 이벤트를 발행하는 구조&lt;/b&gt;로 전환할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 역할은 다음과 같이 분리된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring 서버
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka consumer&lt;/li&gt;
&lt;li&gt;알림 저장 및 상태 갱신&lt;/li&gt;
&lt;li&gt;실제 비즈니스 처리 담당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AWS Lambda
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정해진 시간에 실행&lt;/li&gt;
&lt;li&gt;due target 조회&lt;/li&gt;
&lt;li&gt;Kafka 또는 메시지 큐로 이벤트 발행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조의 핵심은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조회 및 enqueue 역할을 애플리케이션 서버와 분리했다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 Lambda로 분리했는가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 서버 내부 스케줄러 방식은 구현이 단순하다는 장점이 있었지만, 다음과 같은 운영상의 부담이 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스케줄러가 서버 인스턴스 수에 영향을 받는다&lt;/li&gt;
&lt;li&gt;여러 서버가 떠 있으면 분산 락 같은 추가 제어가 필요하다&lt;/li&gt;
&lt;li&gt;조회 로직이 애플리케이션 서버 리소스를 계속 사용한다&lt;/li&gt;
&lt;li&gt;서버 배포/재시작 상태에 따라 스케줄 실행 안정성이 흔들릴 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Lambda로 분리하면, 조회 및 발행 로직을 서버와 독립적으로 실행할 수 있다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Lambda로 분리했을 때의 이점&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;1. 스케줄 실행 주체가 단일화된다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 서버 여러 대가 떠 있어도, 스케줄러 실행 주체는 Lambda 하나로 분리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 애플리케이션 서버에서 &amp;ldquo;누가 스케줄을 실행할 것인가&amp;rdquo;를 더 이상 고민하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 다음과 같은 이점이 생긴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 간 스케줄 실행 경쟁 제거&lt;/li&gt;
&lt;li&gt;전역 Redis 락 의존도 감소&lt;/li&gt;
&lt;li&gt;서버 수 변화와 무관하게 일정한 스케줄 실행 구조 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;2. 애플리케이션 서버의 책임이 줄어든다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 서버는 이제 스케줄을 돌며 조회하는 역할을 하지 않고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;들어온 이벤트를 소비하고 처리하는 역할에 집중하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 서버 책임이 더 명확해진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lambda: 조회 및 이벤트 발행&lt;/li&gt;
&lt;li&gt;Spring Consumer: 이벤트 처리 및 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 역할이 분리되면 애플리케이션 서버는 더 단순해지고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 처리 서버로서의 역할에 집중할 수 있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;3. 서버 배포와 스케줄 실행이 분리된다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 서버를 배포하거나 재시작하는 타이밍이 스케줄 실행에 영향을 줄 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Lambda는 별도 실행 주체이기 때문에, 애플리케이션 서버 배포와 무관하게 스케줄을 안정적으로 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 다음과 같은 장점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 롤링 배포 중에도 조회/발행 흐름 유지 가능&lt;/li&gt;
&lt;li&gt;서버 다운/재기동과 스케줄 실행이 직접적으로 결합되지 않음&lt;/li&gt;
&lt;li&gt;운영 안정성 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;4. 조회 로직의 확장 및 격리가 쉬워진다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 로직은 대량 데이터를 읽고 메시지를 발행하는 별도 성격의 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 Lambda로 분리하면, 조회 로직에 필요한 실행 환경을 Spring 서버와 독립적으로 조절할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 설정&lt;/li&gt;
&lt;li&gt;실행 시간&lt;/li&gt;
&lt;li&gt;재시도 정책&lt;/li&gt;
&lt;li&gt;타임아웃&lt;/li&gt;
&lt;li&gt;스케줄 주기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 Spring 애플리케이션과 별도로 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 조회 로직을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;독립적인 batch/enqueue 계층&lt;/b&gt;으로 분리한 효과를 얻을 수 있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;5. 서버 수 확장과 조회 로직 확장이 서로 얽히지 않는다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구조에서는 서버 수를 늘리면 스케줄러 인스턴스도 함께 늘어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Lambda로 분리하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 수 증가는 consumer 처리량과 연결되고&lt;/li&gt;
&lt;li&gt;조회/이벤트 발행은 Lambda가 독립적으로 담당한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 조회 계층과 처리 계층의 확장 방향을 분리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점은 특히 대규모 처리 환경에서 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Lambda로 조회 및 이벤트 발행 로직을 분리한 것은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 구현 위치를 바꾼 것이 아니라, 전체 아키텍처를 더 명확하게 나누기 위함이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;출처&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafka :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.confluent.io/kafka/overview.html&quot;&gt;https://docs.confluent.io/kafka/overview.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774097217386&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;Kafka | Confluent Documentation&quot; data-og-description=&quot;Apache Kafka Documentation Apache Kafka&amp;reg; is an open-source distributed data streaming engine that thousands of companies use to build streaming data pipelines and applications, powering mission-critical operational and analytics use cases. Learn More&quot; data-og-host=&quot;docs.confluent.io&quot; data-og-source-url=&quot;https://docs.confluent.io/kafka/overview.html&quot; data-og-url=&quot;https://docs.confluent.io/kafka/overview.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/GkJyk/dJMb9hC0yK4/5Sd47P3Bi1m6vmVCRv4KPk/img.png?width=1072&amp;amp;height=260&amp;amp;face=0_0_1072_260&quot;&gt;&lt;a href=&quot;https://docs.confluent.io/kafka/overview.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.confluent.io/kafka/overview.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/GkJyk/dJMb9hC0yK4/5Sd47P3Bi1m6vmVCRv4KPk/img.png?width=1072&amp;amp;height=260&amp;amp;face=0_0_1072_260');&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;Kafka | Confluent Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Apache Kafka Documentation Apache Kafka&amp;reg; is an open-source distributed data streaming engine that thousands of companies use to build streaming data pipelines and applications, powering mission-critical operational and analytics use cases. Learn More&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.confluent.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parallel-consumer :&amp;nbsp;&lt;a href=&quot;https://github.com/confluentinc/parallel-consumer&quot;&gt;https://github.com/confluentinc/parallel-consumer&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774097214197&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - confluentinc/parallel-consumer: Parallel Apache Kafka client wrapper with per message ACK, client side queueing, a simp&quot; data-og-description=&quot;Parallel Apache Kafka client wrapper with per message ACK, client side queueing, a simpler consumer/producer API with key concurrency and extendable non-blocking IO processing. - confluentinc/paral...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/confluentinc/parallel-consumer&quot; data-og-url=&quot;https://github.com/confluentinc/parallel-consumer&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fXl0x/dJMb83ksatz/I7sgLL96y9TAYtSwtfoGHk/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/dnNWkk/dJMb88eZTqK/fLsIDRrNURoxhSRkv07V0k/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/mWS0A/dJMb86O02WZ/bLgCpfRNkzpc48Z0RRaS7k/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=144_162_216_240&quot;&gt;&lt;a href=&quot;https://github.com/confluentinc/parallel-consumer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/confluentinc/parallel-consumer&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fXl0x/dJMb83ksatz/I7sgLL96y9TAYtSwtfoGHk/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/dnNWkk/dJMb88eZTqK/fLsIDRrNURoxhSRkv07V0k/img.png?width=400&amp;amp;height=400&amp;amp;face=0_0_400_400,https://scrap.kakaocdn.net/dn/mWS0A/dJMb86O02WZ/bLgCpfRNkzpc48Z0RRaS7k/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=144_162_216_240');&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;GitHub - confluentinc/parallel-consumer: Parallel Apache Kafka client wrapper with per message ACK, client side queueing, a simp&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Parallel Apache Kafka client wrapper with per message ACK, client side queueing, a simpler consumer/producer API with key concurrency and extendable non-blocking IO processing. - confluentinc/paral...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;naver :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/7181840&quot;&gt;https://d2.naver.com/helloworld/7181840&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;woowacon2025 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=UhnERp2AYRo&amp;amp;t=657s&quot;&gt;https://www.youtube.com/watch?v=UhnERp2AYRo&amp;amp;t=657s&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=UhnERp2AYRo&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/YJTc4/dJMb9hC0yMg/aOndmIDbs90ytirfDOzcF0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;Kafka 파티션 증설 없이 처리량 한계 돌파하기: Parallel Consumer 적용기 #우아콘2025 #우아한형제들&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/UhnERp2AYRo&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;toss :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=v9rcKpUZw4o&amp;amp;t=176s&quot;&gt;https://www.youtube.com/watch?v=v9rcKpUZw4o&amp;amp;t=176s&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=v9rcKpUZw4o&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/di57WR/dJMb81GWt1b/YDSHf2staCDkrU0zBllnek/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=870_208_1100_458&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;토스ㅣSLASH 22 - 왜 은행은 무한스크롤이 안되나요&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/v9rcKpUZw4o&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>카프카</category>
      <category>consumer</category>
      <category>kafka</category>
      <category>parallel</category>
      <category>성능 개선</category>
      <category>알림</category>
      <category>카프카</category>
      <author>27200</author>
      <guid isPermaLink="true">https://to-travel-coding.tistory.com/475</guid>
      <comments>https://to-travel-coding.tistory.com/475#entry475comment</comments>
      <pubDate>Sat, 21 Mar 2026 21:49:40 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(2)</title>
      <link>https://to-travel-coding.tistory.com/474</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;‼️  최종 개선 결과  ‼️&lt;br /&gt;&lt;br /&gt;- 초당 처리량 362.12건 &amp;rarr; 6,767.27건&lt;br /&gt;- 약 18.7배 성능 향상&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;총 3편의 내용으로 구성된 개선 과정이다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편 :&amp;nbsp;&lt;a href=&quot;https://to-travel-coding.tistory.com/473&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(1)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774097509858&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;[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(1)&quot; data-og-description=&quot;‼️  최종 개선 결과  ‼️- 초당 처리량 362.12건 &amp;rarr; 6,767.27건- 약 18.7배 성능 향상개요프로젝트를 진행하는 과정에서 알림 기능을 추가해야 했다.기능 요구조건은 아래와 같았다.사용자 1명&quot; data-og-host=&quot;to-travel-coding.tistory.com&quot; data-og-source-url=&quot;https://to-travel-coding.tistory.com/473&quot; data-og-url=&quot;https://to-travel-coding.tistory.com/473&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/EpXyc/dJMb9lk6pvg/c58QL0CyExQ72ANXHRULKk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cR8vH4/dJMb9lk6pvf/qIk7k2HMwLaYqFQNvS0xcK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cocEM4/dJMb9efddbe/bwtem9bECzxoMRRYVlozeK/img.jpg?width=2048&amp;amp;height=2048&amp;amp;face=0_0_2048_2048&quot;&gt;&lt;a href=&quot;https://to-travel-coding.tistory.com/473&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://to-travel-coding.tistory.com/473&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/EpXyc/dJMb9lk6pvg/c58QL0CyExQ72ANXHRULKk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cR8vH4/dJMb9lk6pvf/qIk7k2HMwLaYqFQNvS0xcK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cocEM4/dJMb9efddbe/bwtem9bECzxoMRRYVlozeK/img.jpg?width=2048&amp;amp;height=2048&amp;amp;face=0_0_2048_2048');&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;[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;‼️  최종 개선 결과  ‼️- 초당 처리량 362.12건 &amp;rarr; 6,767.27건- 약 18.7배 성능 향상개요프로젝트를 진행하는 과정에서 알림 기능을 추가해야 했다.기능 요구조건은 아래와 같았다.사용자 1명&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;to-travel-coding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편(완) : &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://to-travel-coding.tistory.com/475&quot;&gt;2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(3(완))&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774097528571&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;[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(3(완))&quot; data-og-description=&quot;‼️  최종 개선 결과  ‼️- 초당 처리량 362.12건 &amp;rarr; 6,767.27건- 약 18.7배 성능 향상총 3편의 내용으로 구성된 개선 과정이다.더보기2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능 &quot; data-og-host=&quot;to-travel-coding.tistory.com&quot; data-og-source-url=&quot;https://to-travel-coding.tistory.com/475&quot; data-og-url=&quot;https://to-travel-coding.tistory.com/475&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/einQOt/dJMb8952R8h/YDfytQj5Ju2iqh1hcLB1u0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/beyVgD/dJMb9g5aqR4/NM1fRwvA2C0AV2KTpwXJpk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b7epNd/dJMb9kT2fNr/QZayjyXxFKTc3y4zqKqk60/img.jpg?width=2048&amp;amp;height=2048&amp;amp;face=0_0_2048_2048&quot;&gt;&lt;a href=&quot;https://to-travel-coding.tistory.com/475&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://to-travel-coding.tistory.com/475&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/einQOt/dJMb8952R8h/YDfytQj5Ju2iqh1hcLB1u0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/beyVgD/dJMb9g5aqR4/NM1fRwvA2C0AV2KTpwXJpk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b7epNd/dJMb9kT2fNr/QZayjyXxFKTc3y4zqKqk60/img.jpg?width=2048&amp;amp;height=2048&amp;amp;face=0_0_2048_2048');&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;[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(3(완))&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;‼️  최종 개선 결과  ‼️- 초당 처리량 362.12건 &amp;rarr; 6,767.27건- 약 18.7배 성능 향상총 3편의 내용으로 구성된 개선 과정이다.더보기2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;to-travel-coding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3차 구현&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;조회는 단일 서버, 처리는 Kafka Consumer 분산 처리&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2차 구현에서는 target 단위 Redis 분산 락을 통해 중복 처리를 줄였지만,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여전히 모든 서버가 동시에 due target을 조회한다는 문제가 남아 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉 처리 중복은 줄일 수 있었지만, 조회 부하는 서버 수만큼 반복될 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 3차 구현에서는 역할을 다음과 같이 분리했다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현 방식&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 조회 단계&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스케줄러가 due target을 조회한다.&lt;/li&gt;
&lt;li&gt;이 단계는 Redis 기반 전역 분산 락을 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;오직 한 서버만 수행&lt;/b&gt;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 메시지 적재 단계&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회된 target을 Kafka 이벤트로 발행한다.&lt;/li&gt;
&lt;li&gt;각 target은 개별 메시지로 큐에 적재된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 처리 단계&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 서버에 떠 있는 Kafka consumer가 메시지를 분산 소비한다.&lt;/li&gt;
&lt;li&gt;각 consumer는 자신이 가져온 메시지에 대해서만 DB 저장 및 상태 갱신을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1835&quot; data-origin-height=&quot;1436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9wBA4/dJMcahDJy4E/bajqTuS7luO55kp4U4K6Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9wBA4/dJMcahDJy4E/bajqTuS7luO55kp4U4K6Mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9wBA4/dJMcahDJy4E/bajqTuS7luO55kp4U4K6Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9wBA4%2FdJMcahDJy4E%2FbajqTuS7luO55kp4U4K6Mk%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;1835&quot; height=&quot;1436&quot; data-origin-width=&quot;1835&quot; data-origin-height=&quot;1436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;장점&lt;/span&gt;은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;due target 조회는 한 서버만 수행하므로 DB 조회 중복이 제거된다.&lt;/li&gt;
&lt;li&gt;실제 처리 단계는 Kafka consumer group을 통해 여러 서버가 병렬 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;처리량 확장이 쉽다.&lt;/li&gt;
&lt;li&gt;target별 분산 락 없이도 consumer group 기준으로 메시지 분산 처리가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만 다음과 같은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f89009;&quot;&gt;보완&lt;/span&gt;은 여전히 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;consumer 장애 시 재처리 전략&lt;/li&gt;
&lt;li&gt;발행 실패/소비 실패에 대한 DLQ 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정리하면, 3차 구현은 다음과 같은 방향이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회: 전역 Redis 락으로 단일 서버만 수행&lt;/li&gt;
&lt;li&gt;처리: Kafka consumer group으로 분산 처리&lt;/li&gt;
&lt;li&gt;확장성: 서버 수 증가에 따라 처리량 확장 가능&lt;/li&gt;
&lt;li&gt;중복 방지: 조회 단계는 분산 락, 처리 단계는 queue consumer로 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보완점이 분명 존재하지만 이는 카프카 자체에 대한 에러 핸들링으로 추후 고려하면 큰 문제가 되지 않을 것이라고 판단하였다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4차 구현&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여러 가지 문제가 해결되었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;하나의 target에 대한 단일 알림이 보장된다.&lt;/li&gt;
&lt;li&gt;중복 조회를 하지 않아 db 부하가 감소한다.&lt;/li&gt;
&lt;li&gt;배치 처리를 통해 db 조회를 효율적으로 진행한다.(주요 흐름은 아니라 별도 기술하지는 않았다.)&lt;/li&gt;
&lt;li&gt;컨슈머 그룹 내의 파티션 단위로 병렬 처리가 가능하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트 및 문제&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 기능 구현이 완료되었다 생각했고, 테스트를 진행해 보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVX1P0/dJMcad2m4v4/cnAXk5tL22F8bp3a9kHheK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVX1P0/dJMcad2m4v4/cnAXk5tL22F8bp3a9kHheK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVX1P0/dJMcad2m4v4/cnAXk5tL22F8bp3a9kHheK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVX1P0%2FdJMcad2m4v4%2FcnAXk5tL22F8bp3a9kHheK%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;528&quot; height=&quot;84&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;[GREENROOM KAFKA BENCHMARK] 
eligible=100000, 
outboxCreated=100000, 
eventPublishSec=0.578,  
totalSec=276.153, 
msgsPerSecond=362.12&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이벤트를 발행하는 시간은 크게 소요되지 않았지만, 이를 처리하고 데이터베이스에 적재하는 작업까지의 시간이 오래 소요되었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;10만 건의 알림을 처리하는 데 있어 약 5분이 소요된 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 362.12 * 60 인 약 18000명 정도의 유저만 정각에 알림을 받고, 지연이 발생하는 문제가 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;db저장이 제일 큰 병목이었기에 이를 비동기로 처리할까 했지만, 사용자가 db 조회를 통해 알림 정보를 확인하는 만큼 결과적으로 똑같을 것이라 판단하였다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제 해결&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존 Kafka consumer 구현은 메시지를 하나 consume할 때마다 즉시 DB에 저장하는 구조였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 구현은 단순하지만, 메시지 수가 많아질수록&lt;span&gt;&amp;nbsp;&lt;/span&gt;insert/update/flush가 매우 자주 발생하여 DB 부하가 커질 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 줄이기 위해 Kafka consumer에서 메시지를 바로 저장하지 않고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;일정 개수만큼 모아서 배치 저장&lt;/b&gt;&lt;/span&gt;하는 방식을 적용했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처리 방식은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Kafka consumer가 메시지를 consume한다.&lt;/li&gt;
&lt;li&gt;consume한 메시지를 메모리 버퍼에 적재한다.&lt;/li&gt;
&lt;li&gt;버퍼에 200건이 쌓이면 한 번에 DB 저장을 수행한다.&lt;/li&gt;
&lt;li&gt;메시지가 더 이상 들어오지 않는 idle 상태가 되면, 200건이 되지 않았더라도 남은 메시지를 저장한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;장점&lt;/span&gt;은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지마다 DB 쓰기를 수행하지 않으므로 DB 부하를 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;마지막에 남은 소량의 데이터도 idle flush로 유실 없이 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 idle 상태란 다음을 의미한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마지막 메시지를 consume한 이후 일정 시간 동안 새로운 메시지가 들어오지 않는 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 마지막 메시지를 받은 뒤 1초 동안 새 메시지가 없다면,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;버퍼에 200건이 모이지 않았더라도 남은 데이터를 모두 DB에 저장한다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; 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-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;979&quot; data-origin-height=&quot;1320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dym8F4/dJMcagSos0s/ZWCpTBY6Ym3GOpkJsYIZP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dym8F4/dJMcagSos0s/ZWCpTBY6Ym3GOpkJsYIZP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dym8F4/dJMcagSos0s/ZWCpTBY6Ym3GOpkJsYIZP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdym8F4%2FdJMcagSos0s%2FZWCpTBY6Ym3GOpkJsYIZP0%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;979&quot; height=&quot;1320&quot; data-origin-width=&quot;979&quot; data-origin-height=&quot;1320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;485&quot; data-origin-height=&quot;85&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YIXFK/dJMcafFWQ0w/ivE8l4nKl4pYcRlLL28ykK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YIXFK/dJMcafFWQ0w/ivE8l4nKl4pYcRlLL28ykK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YIXFK/dJMcafFWQ0w/ivE8l4nKl4pYcRlLL28ykK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYIXFK%2FdJMcafFWQ0w%2FivE8l4nKl4pYcRlLL28ykK%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;485&quot; height=&quot;85&quot; data-origin-width=&quot;485&quot; data-origin-height=&quot;85&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[GREENROOM KAFKA BATCH BENCHMARK] 
eligible=100000, 
outboxCreated=100000, 
eventPublishSec=0.649, 
totalSec=38.944, 
msgsPerSecond=2567.79&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정말 끝?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;거의 모든 문제가 해결되었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;하지만 이러한 궁금증이 남았다. 10만 건의 알림이 아니라, 더 많은 알림을 전송해야 한다면 어떻게 해야 할까?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;Kafka 토픽의 파티션 수를 늘린다.&lt;/li&gt;
&lt;li&gt;이를 처리할 서버 수를 늘린다.&lt;/li&gt;
&lt;li&gt;서버를 늘릴 수 없다면, 하나의 서버에서 더 많은 스레드로 여러 파티션을 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #f89009;&quot;&gt;&lt;b&gt;파티션을 추가하는 것은 생각보다 단순한 작업이 아니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파티션 수를 늘리면 처리량과 병렬성은 높일 수 있지만, 동시에 다음과 같은 문제를 같이 고려해야 한다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1. 메시지 순서 보장 문제가 달라진다&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Kafka는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;파티션 내부에서만 순서를 보장&lt;/b&gt;한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉 파티션 수를 늘리면, 같은 키를 가진 메시지가 새로운 파티션 정책에 따라 다른 파티션으로 분산될 수 있고, 이 경우 기존과 같은 순서 보장을 기대하기 어려워질 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 기존에 key 기반 파티셔닝을 사용하고 있었다면, 파티션 수를 변경하는 순간 같은 key의 메시지가 이전과 다른 파티션으로 라우팅 될 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Confluent 문서도 파티션 수를 증가시킬 때 keyed message의 ordering guarantee에 주의해야 한다고 설명하고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Confluent Docs:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.confluent.io/kafka/operations-tools/partition-determination.html&quot;&gt;Choose and Change the Partition Count in Kafka&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2. Consumer 수를 늘린다고 무조건 성능이 선형 증가하지 않는다&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Kafka에서 병렬성의 기본 단위는 partition이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉 파티션이 5개면 같은 consumer group 안에서 동시에 의미 있게 일하는 consumer도 최대 5개다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 이상 consumer를 늘려도 일부는 idle 상태가 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Apache Kafka 공식 문서도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;consumer 수가 partition 수보다 많으면 일부 consumer는 아무 일도 하지 않게 된다&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;Apache Kafka Docs:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://kafka.apache.org/0100/documentation/&quot;&gt;Kafka Consumers and Rebalancing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉 단순히 서버나 스레드를 늘리는 것만으로는 성능이 무한히 늘어나지 않는다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;3. 파티션 수가 많아질수록 운영 비용도 증가한다&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파티션은 단순한 논리 단위가 아니라, 브로커 입장에서는 메타데이터, 파일 핸들, 세그먼트 관리, 복제 부담을 모두 증가시키는 대상이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉 파티션 수가 너무 많아지면 브로커 메모리 사용량, 네트워크 I/O, 디스크 I/O, 리밸런싱 비용이 커질 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Confluent 문서에서도 파티션은 throughput을 높이는 수단이지만, producer/consumer/broker 설정과 처리 로직을 함께 고려해야 한다고 설명한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Confluent Docs:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.confluent.io/kafka/operations-tools/partition-determination.html&quot;&gt;Choose and Change the Partition Count in Kafka&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;4. Rebalancing 비용이 커질 수 있다&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파티션 수를 늘리거나 consumer 수를 바꾸면 consumer group rebalancing이 발생한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 과정에서 일시적으로 소비 지연이 생길 수 있고, 처리 중단 구간이 길어질 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 대규모 토픽과 많은 consumer가 붙은 환경에서는 리밸런싱 자체가 운영상 부담이 될 수 있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;5. 파티션 증설은 쉽지만, 되돌리기는 쉽지 않다&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Kafka는 보통 기존 토픽의 파티션 수를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;늘릴 수는 있어도 줄이기는 어렵다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉 처음에 너무 단순하게 파티션을 늘렸다가, 이후 운영상 부담이 커져도 쉽게 되돌릴 수 없는 경우가 많다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Confluent Cloud FAQ에서도 기존 토픽의 partition 수는 증가 가능하지만 감소는 불가능하다고 설명한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Confluent Cloud FAQ:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.confluent.io/cloud/current/topics/topics-faq.html&quot;&gt;Can I change the number of partitions for an existing topic?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>카프카</category>
      <category>consumer</category>
      <category>parallel</category>
      <category>parallel consumer</category>
      <category>성능 개선</category>
      <category>알림</category>
      <category>카프카</category>
      <author>27200</author>
      <guid isPermaLink="true">https://to-travel-coding.tistory.com/474</guid>
      <comments>https://to-travel-coding.tistory.com/474#entry474comment</comments>
      <pubDate>Sat, 21 Mar 2026 21:48:39 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(1)</title>
      <link>https://to-travel-coding.tistory.com/473</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;‼️  최종 개선 결과  ‼️&lt;br /&gt;&lt;br /&gt;- 초당 처리량 362.12건 &amp;rarr; 6,767.27건&lt;br /&gt;- 약 18.7배 성능 향상&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;총 3편의 내용으로 구성된 개선 과정이다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2편 :&lt;/p&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://to-travel-coding.tistory.com/474&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(2)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774097669386&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;[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(2)&quot; data-og-description=&quot;‼️  최종 개선 결과  ‼️- 초당 처리량 362.12건 &amp;rarr; 6,767.27건- 약 18.7배 성능 향상총 3편의 내용이다.3차 구현조회는 단일 서버, 처리는 Kafka Consumer 분산 처리 2차 구현에서는 target 단위 Redis &quot; data-og-host=&quot;to-travel-coding.tistory.com&quot; data-og-source-url=&quot;https://to-travel-coding.tistory.com/474&quot; data-og-url=&quot;https://to-travel-coding.tistory.com/474&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cxZTVg/dJMb8XkeCwA/fYOSQoxF32v8j6RIzI9F5k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cMc1Qb/dJMb8WMoQtT/TXEAukmdIirV5GaIxuYcLk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/MKXth/dJMb8PGvp1v/7bVEgA9CYBsfkvgKsxre7k/img.jpg?width=2048&amp;amp;height=2048&amp;amp;face=0_0_2048_2048&quot;&gt;&lt;a href=&quot;https://to-travel-coding.tistory.com/474&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://to-travel-coding.tistory.com/474&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cxZTVg/dJMb8XkeCwA/fYOSQoxF32v8j6RIzI9F5k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cMc1Qb/dJMb8WMoQtT/TXEAukmdIirV5GaIxuYcLk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/MKXth/dJMb8PGvp1v/7bVEgA9CYBsfkvgKsxre7k/img.jpg?width=2048&amp;amp;height=2048&amp;amp;face=0_0_2048_2048');&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;[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(2)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;‼️  최종 개선 결과  ‼️- 초당 처리량 362.12건 &amp;rarr; 6,767.27건- 약 18.7배 성능 향상총 3편의 내용이다.3차 구현조회는 단일 서버, 처리는 Kafka Consumer 분산 처리 2차 구현에서는 target 단위 Redis&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;to-travel-coding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편(완) :&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://to-travel-coding.tistory.com/475&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(3(완))&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774097679523&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;[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(3(완))&quot; data-og-description=&quot;‼️  최종 개선 결과  ‼️- 초당 처리량 362.12건 &amp;rarr; 6,767.27건- 약 18.7배 성능 향상총 3편의 내용으로 구성된 개선 과정이다.더보기2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능 &quot; data-og-host=&quot;to-travel-coding.tistory.com&quot; data-og-source-url=&quot;https://to-travel-coding.tistory.com/475&quot; data-og-url=&quot;https://to-travel-coding.tistory.com/475&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/einQOt/dJMb8952R8h/YDfytQj5Ju2iqh1hcLB1u0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/beyVgD/dJMb9g5aqR4/NM1fRwvA2C0AV2KTpwXJpk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b7epNd/dJMb9kT2fNr/QZayjyXxFKTc3y4zqKqk60/img.jpg?width=2048&amp;amp;height=2048&amp;amp;face=0_0_2048_2048&quot;&gt;&lt;a href=&quot;https://to-travel-coding.tistory.com/475&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://to-travel-coding.tistory.com/475&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/einQOt/dJMb8952R8h/YDfytQj5Ju2iqh1hcLB1u0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/beyVgD/dJMb9g5aqR4/NM1fRwvA2C0AV2KTpwXJpk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b7epNd/dJMb9kT2fNr/QZayjyXxFKTc3y4zqKqk60/img.jpg?width=2048&amp;amp;height=2048&amp;amp;face=0_0_2048_2048');&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;[Kafka] Parallel-Consumer을 통한 알림 성능 개선 과정(3(완))&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;‼️  최종 개선 결과  ‼️- 초당 처리량 362.12건 &amp;rarr; 6,767.27건- 약 18.7배 성능 향상총 3편의 내용으로 구성된 개선 과정이다.더보기2026.03.21 - [카프카] - [Kafka] Parallel-Consumer을 통한 알림 성능&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;to-travel-coding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하는 과정에서 알림 기능을 추가해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 요구조건은 아래와 같았다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자 1명당 N개의 알림을 받을 수 있다.&lt;/li&gt;
&lt;li&gt;추후 알림 별 시간 설정 기능이 추가될 수 있으나, 현재 모든 알림이 동일한 시간에 제공된다.&lt;/li&gt;
&lt;li&gt;사용자는 알림 수신 여부를 설정할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;알림 수신 여부 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FLgI8/dJMcaiP9EVs/3OlyqbN5eNPwhlMKCAjj00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FLgI8/dJMcaiP9EVs/3OlyqbN5eNPwhlMKCAjj00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FLgI8/dJMcaiP9EVs/3OlyqbN5eNPwhlMKCAjj00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFLgI8%2FdJMcaiP9EVs%2F3OlyqbN5eNPwhlMKCAjj00%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;1038&quot; height=&quot;545&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 최초 로그인을 할 때 필수 약관 동의 및 알림 수신 정보 등록을 요청한다.&lt;/li&gt;
&lt;li&gt;알림 수신 정보 이벤트를 발행한다.&lt;/li&gt;
&lt;li&gt;Notification 서버는 이를 받아 알림 수신 정보를 저장한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1차 구현&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional
@Scheduled(cron = &quot;0 30 8 * * *&quot;, zone = &quot;Asia/Seoul&quot;)
public void run() {
    Instant now = Instant.now();
    targetRepository.findByResolvedFalseAndEnabledTrueAndNextSendAtLessThanEqual(now).forEach(target -&amp;gt; {
        boolean success = dispatchService.sendNotification(target.getUserId(), target.getTicketId(), target.getNextSequence());
        if (success) {
            target.advanceAfterSuccess();
            targetRepository.save(target);
        }
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현은 위와 같았다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터베이스에서 알림 대상이 되는 타겟을 먼저 찾는다.&lt;/li&gt;
&lt;li&gt;각 타겟을 직접 sendNotification 메서드를 통해 전송한다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현재 실제 SSE 발송은 없고, db 저장만 진행한다.&lt;/li&gt;
&lt;/ol&gt;
&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;한 트랜잭션 내부에 너무 많은 작업이 묶인다.&lt;/li&gt;
&lt;li&gt;배치 청크 처리 작업이 없어 db 부하가 커질 수 있다.&lt;/li&gt;
&lt;li&gt;실패 처리가 존재하지 않는다.&lt;/li&gt;
&lt;li&gt;서버가 여러 대 구동되어 스케쥴러가 동시 작동한다면 중복처리 문제가 발생할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제 해결&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;알림 중복 해결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 서버에서 동일한 스케줄러가 동시에 실행될 경우, 같은 알림 대상이 중복 처리될 수 있다는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 기존 구현은 단순 조회 후 바로 처리하는 방식이었기 때문에, 서버 A와 서버 B가 같은 시점에 동일한 target을 조회하면 둘 다 같은 알림을 저장하거나 발송할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해 서버 단위 Redis 기반 분산 락을 적용했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;1691&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dah0Da/dJMcahX3qdG/kvJqyikmtrZ97TxkbzOkx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dah0Da/dJMcahX3qdG/kvJqyikmtrZ97TxkbzOkx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dah0Da/dJMcahX3qdG/kvJqyikmtrZ97TxkbzOkx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdah0Da%2FdJMcahX3qdG%2FkvJqyikmtrZ97TxkbzOkx1%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;1308&quot; height=&quot;1691&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;1691&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;// 예시 코드
public void run_db_only_with_redis_lock() {
    String token = distributedLockService.tryLock(&quot;scheduler:run-db-only&quot;, Duration.ofMinutes(5));
    if (token == null) {
        return;
    }

    try {
        run_db_only();
    } finally {
        distributedLockService.unlock(&quot;scheduler:run-db-only&quot;, token);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리 방식은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스케줄러 실행 전 Redis에 특정 lock key를 기준으로 락 획득을 시도한다.&lt;/li&gt;
&lt;li&gt;락 획득에 성공한 서버만 실제 알림 처리 로직을 수행한다.&lt;/li&gt;
&lt;li&gt;락 획득에 실패한 서버는 해당 스케줄 실행을 즉시 종료한다.&lt;/li&gt;
&lt;li&gt;작업이 끝나면 락을 해제한다.&lt;/li&gt;
&lt;li&gt;락 해제 시에는 token 기반 검증을 사용하여, 자신이 획득한 락만 해제할 수 있도록 한다.&lt;/li&gt;
&lt;/ol&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;Redis &lt;code&gt;SETNX&lt;/code&gt; 기반 락 획득&lt;/li&gt;
&lt;li&gt;TTL을 포함한 락 만료 시간 설정&lt;/li&gt;
&lt;li&gt;Lua Script를 이용한 안전한 unlock 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 방식은 스케줄러 전체를 한 번에 잠그는 방식이기 때문에, 향후 처리량이 더 커질 경우에는 target 단위 처리나 큐 기반 분산 처리 구조로 확장하는 것이 더 적합할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2차 구현&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 구현에서는 스케줄러 전체에 대해 하나의 분산 락을 거는 방식으로 중복 실행을 막았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 구현이 단순하고 빠르게 적용할 수 있다는 장점이 있었지만, 스케줄러 전체가 하나의 락에 묶이기 때문에 병렬 처리에 제약이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 개선하기 위해 2차 구현에서는 &lt;b&gt;타겟별 분산 락&lt;/b&gt;을 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리 흐름은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스케줄러가 due target 목록을 조회한다.&lt;/li&gt;
&lt;li&gt;각 target에 대해 &lt;code&gt;ticketId&lt;/code&gt; 기준으로 개별 분산 락 획득을 시도한다.&lt;/li&gt;
&lt;li&gt;락 획득에 성공한 target만 실제 처리한다.&lt;/li&gt;
&lt;li&gt;처리 성공 시 알림 정보와 target 상태를 갱신한다.&lt;/li&gt;
&lt;li&gt;처리 종료 후 해당 target의 락을 해제한다.&lt;/li&gt;
&lt;li&gt;락 획득에 실패한 target은 이미 다른 서버가 처리 중인 것으로 보고 건너뛴다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 &lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/span&gt;은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스케줄러 전체를 하나의 락으로 막지 않아도 된다.&lt;/li&gt;
&lt;li&gt;여러 서버가 동시에 실행되더라도 서로 다른 target은 병렬 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;동일한 target만 중복 처리되지 않도록 제어할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &lt;span style=&quot;background-color: #f89009;&quot;&gt;주의&lt;/span&gt;할 점도 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;락 key 설계를 명확히 해야 한다.&lt;/li&gt;
&lt;li&gt;락 TTL이 너무 짧으면 처리 중 락이 풀릴 수 있다.&lt;/li&gt;
&lt;li&gt;처리 시간이 긴 경우 TTL 연장 전략이 필요할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 락 key는 다음과 같이 구성하였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;LOCK:GREENROOM:TARGET:{ticketId}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 같은 &lt;code&gt;ticketId&lt;/code&gt;에 대해서만 상호 배타 처리가 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1261&quot; data-origin-height=&quot;1688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cifWF8/dJMcaakkHGE/lJ1oPzlcdk3CcUvXqSbJJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cifWF8/dJMcaakkHGE/lJ1oPzlcdk3CcUvXqSbJJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cifWF8/dJMcaakkHGE/lJ1oPzlcdk3CcUvXqSbJJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcifWF8%2FdJMcaakkHGE%2FlJ1oPzlcdk3CcUvXqSbJJk%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;1261&quot; height=&quot;1688&quot; data-origin-width=&quot;1261&quot; data-origin-height=&quot;1688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;// 예시 코드
@Transactional
public void run_with_target_lock() {
    Instant now = Instant.now();

    targetRepository.findByResolvedFalseAndEnabledTrueAndNextSendAtLessThanEqual(now).forEach(target -&amp;gt; {
        String lockName = &quot;TARGET:&quot; + target.getTicketId();
        String token = distributedLockService.tryLock(lockName, Duration.ofMinutes(5));

        if (token == null) {
            return;
        }

        try {
            boolean success = dispatchService.sendNotification(
                target.getUserId(),
                target.getTicketId(),
                target.getNextSequence()
            );

            if (success) {
                target.advanceAfterSuccess();
                targetRepository.save(target);
            }
        } finally {
            distributedLockService.unlock(lockName, token);
        }
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2차 구현의 한계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;타겟별 분산 락 방식은 동일한 target에 대한 중복 처리를 방지하는 데에는 효과적&lt;/span&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 여러 서버가 동시에 스케줄러를 실행하더라도, 같은 &lt;code&gt;ticketId&lt;/code&gt;에 대해서는 하나의 서버만 실제 처리하도록 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 방식에도 여전히 중요한 한계가 존재했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 문제는 &lt;span style=&quot;background-color: #f89009;&quot;&gt;&lt;b&gt;모든 서버가 스케줄러 시작 시점에 동일한 데이터베이스 조회를 수행한다는 점&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리 흐름을 보면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버 A, B, C가 같은 시각에 스케줄러를 시작한다.&lt;/li&gt;
&lt;li&gt;각 서버는 모두 due target 조회 쿼리를 실행한다.&lt;/li&gt;
&lt;li&gt;조회된 target에 대해 각자 Redis 락 획득을 시도한다.&lt;/li&gt;
&lt;li&gt;락을 획득한 서버만 처리하고, 나머지는 해당 target을 건너뛴다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;span style=&quot;background-color: #f89009;&quot;&gt;&lt;b&gt;실제 처리 중복은 줄어들었지만, 조회 부하는 그대로 중복 발생한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식에서 발생할 수 있는 문제는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 서버가 동일한 due target 목록을 동시에 조회하므로 DB read 부하가 커진다.&lt;/li&gt;
&lt;li&gt;target 수가 많아질수록 각 서버가 불필요하게 대량 데이터를 읽게 된다.&lt;/li&gt;
&lt;li&gt;결국 처리 자체는 한 번만 일어나더라도, 조회 비용은 서버 수만큼 반복된다.&lt;/li&gt;
&lt;li&gt;예를 들어 서버가 5대이고 due target이 10만 건이라면, 최악의 경우 동일한 10만 건 조회가 5번 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;이는 ticketId 기반 Redis 락으로는 해결되지 않는 문제이며, DB에 불필요한 부하를 유발한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 추가적인 비효율도 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회 후 대부분의 target에서 락 획득에 실패할 수 있으므로, 읽어온 데이터의 상당수가 실제 처리되지 않고 버려질 수 있다.&lt;/li&gt;
&lt;li&gt;Redis 락 획득 시도 자체도 target 수만큼 발생하므로, Redis 부하 역시 커질 수 있다.&lt;/li&gt;
&lt;li&gt;즉 중복 처리 방지는 가능하지만, 전체 시스템 관점에서는 조회 비용과 락 경쟁 비용이 여전히 크다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, 2차 구현은 다음과 같은 특징을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일 target 중복 처리 방지&lt;/li&gt;
&lt;li&gt;서로 다른 target은 병렬 처리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;한계:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 서버가 동일한 due target을 동시에 조회&lt;/li&gt;
&lt;li&gt;DB 조회 부하가 서버 수만큼 증가 가능&lt;/li&gt;
&lt;li&gt;락 획득 실패 대상에 대해서도 조회 비용이 이미 발생&lt;/li&gt;
&lt;li&gt;대규모 트래픽 환경에서는 비효율이 커질 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 방식은 &lt;u&gt;&lt;b&gt;중복 처리 방지에는 유효하지만, 조회 비용 최적화까지 해결한 구조는 아니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 근본적으로 해결하려면 다음과 같은 구조가 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스케줄러 자체를 단일 실행으로 제한하는 전역 분산 락 방식&lt;/li&gt;
&lt;li&gt;또는 스케줄러는 enqueue만 수행하고 실제 처리는 메시지 큐 consumer가 담당하는 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 2차 구현은 1차 구현보다 처리 단위는 세밀해졌지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;모든 서버가 동시에 DB를 조회한다는 구조적 한계는 여전히 남아 있다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>카프카</category>
      <category>consumer</category>
      <category>kafka</category>
      <category>parallel</category>
      <category>parallel consumer</category>
      <category>대용량</category>
      <category>알림</category>
      <author>27200</author>
      <guid isPermaLink="true">https://to-travel-coding.tistory.com/473</guid>
      <comments>https://to-travel-coding.tistory.com/473#entry473comment</comments>
      <pubDate>Sat, 21 Mar 2026 16:50:48 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Transactional의 이해</title>
      <link>https://to-travel-coding.tistory.com/472</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  트랜잭션&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  트랜잭션이란?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;트랜잭션이란 데이터베이스 혹은 유사 시스템에서 하나의 상호작용 단위이다. &lt;br /&gt;이는 데이터의 정합성을 보장하기 위해 고안된 방법이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 데이터베이스를 일관성 있게 유지하고, 동시 접근하는 여러 프로그램 간의 격리를 제공하기 위해 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 내 SQL문들은 모두 성공하여 commit 되거나, 하나라도 실패하는 경우 전체가 rollback 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  ACID&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;트랜잭션이 어떤 속성을 지녀야 하는지 나타내는 핵심이다.&lt;br /&gt;4가지 규칙을 모두 보장해 안전하게 수행되어야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원자성(Atomicity)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;All or Noting&lt;/li&gt;
&lt;li&gt;모두 성공 or 모두 실패&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 트랜잭션은 논리적으로 나누어질 수 없는 단위이기 때문에 동일한 경과를 보장해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 일부만 성공하거나 실패하는 상태는 존재해서 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;일관성(Consistency)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 이전과 이후 데이터베이스는 항상 일관적인 상태여야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션은 DB를 Consistency -&amp;gt; Consistency로 변경한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 DB의 정의된 규칙을 위반한다면 트랜잭션은 롤백되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 계좌 잔고가 -가 될 수 없도록 설정해 두었다면 이를 일관성 있게 지켜야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;독립성(Isolation)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 트랜잭션이 동시 실행되어도, 하나의 트랜잭션은 독립적으로 동작하는 것이어야 한다.&lt;/li&gt;
&lt;li&gt;트랜잭션 밖에 있는 어떠한 연산도 트랜잭션의 중간 연산을 볼 수 없어야 한다.&lt;/li&gt;
&lt;li&gt;DBMS는 여러 종류의 isolation level을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;지속성(Durability)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성공적으로 수행된 트랜잭션은 영구적으로 반영되어야 한다.&lt;/li&gt;
&lt;li&gt;전원이 꺼진 뒤에도 저장되는 비휘발성 메모리에 저장되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JDBC Template&lt;/h2&gt;
&lt;pre id=&quot;code_1772415411180&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;catch (SQLException e) {
    try {
        connection.rollback();
    } catch (SQLException ex) {
        ex.addSuppressed(e);
        throw ex;
    }
    throw e;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;383&quot; data-start=&quot;370&quot; data-ke-size=&quot;size20&quot;&gt;  단계별 의미&lt;/h4&gt;
&lt;p data-end=&quot;431&quot; data-start=&quot;385&quot; data-ke-size=&quot;size16&quot;&gt;1️⃣ dataSource.getConnection()&lt;br /&gt;&amp;rarr; DB 커넥션 획득&lt;/p&gt;
&lt;p data-end=&quot;431&quot; data-start=&quot;385&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;482&quot; data-start=&quot;433&quot; data-ke-size=&quot;size16&quot;&gt;2️⃣ setAutoCommit(false)&lt;br /&gt;&amp;rarr; 자동 커밋 끄기 (트랜잭션 시작)&lt;/p&gt;
&lt;p data-end=&quot;482&quot; data-start=&quot;433&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;512&quot; data-start=&quot;484&quot; data-ke-size=&quot;size16&quot;&gt;3️⃣ 비즈니스 로직 수행&lt;br /&gt;&amp;rarr; 여러 SQL 실행&lt;/p&gt;
&lt;p data-end=&quot;512&quot; data-start=&quot;484&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;546&quot; data-start=&quot;514&quot; data-ke-size=&quot;size16&quot;&gt;4️⃣ commit()&lt;br /&gt;&amp;rarr; 정상 종료 시 DB 반영&lt;/p&gt;
&lt;p data-end=&quot;546&quot; data-start=&quot;514&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;581&quot; data-start=&quot;548&quot; data-ke-size=&quot;size16&quot;&gt;5️⃣ rollback()&lt;br /&gt;&amp;rarr; 예외 발생 시 되돌리기&lt;/p&gt;
&lt;p data-end=&quot;581&quot; data-start=&quot;548&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;619&quot; data-start=&quot;583&quot; data-ke-size=&quot;size16&quot;&gt;6️⃣ close()&lt;br /&gt;&amp;rarr; 커넥션 반환 (보통 풀로 반환됨)&lt;/p&gt;
&lt;p data-end=&quot;619&quot; data-start=&quot;583&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;619&quot; data-start=&quot;583&quot; data-ke-size=&quot;size16&quot;&gt;즉, 트랜잭션 경계를 개발자가 직접 제어하는 방식이다.&lt;/p&gt;
&lt;p data-end=&quot;619&quot; data-start=&quot;583&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;619&quot; data-start=&quot;583&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭션 범위가 메서드 내부에 고정되거나, AOP 기반 트랜잭션이 불가능하여 자주 사용되지는 않는다.&lt;/p&gt;
&lt;p data-end=&quot;619&quot; data-start=&quot;583&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;619&quot; data-start=&quot;583&quot; data-ke-size=&quot;size26&quot;&gt;‼️ Spring Transaction&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;스프링이 제공하는 추상화된 트랜잭션 관리 기능으로 개발자가 비즈니스 로직에만 집중할 수 있도록&lt;br /&gt;트랜잭션 시작/커밋/롤백을 자동으로 처리해 주는 기능이다.&lt;/blockquote&gt;
&lt;h3 data-end=&quot;619&quot; data-start=&quot;583&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PlatformTransactionManager&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 트랜잭션 추상화의 중심 인터페이스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;commit와 rollback 메서드를 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC, JPA 등 모두 이를 구현하여 Spring Transaction을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 기술에 독립적이고, 트랜잭션 경계설정이 가능해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;org.springframework.transaction.PlatformTransactionManager 경로에 존재하며&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;현재 활성화된 트랜잭션을 반환하거나, 지정된 전파(propagation) 동작에 따라 새로운 트랜잭션을 생성합니다.&lt;br /&gt;&lt;br /&gt;격리 수준(isolation level)이나 타임아웃(timeout)과 같은 매개변수는 새로운 트랜잭션에만 적용되며, 이미 활성화된 트랜잭션에 참여하는 경우에는 무시됩니다.&lt;br /&gt;&lt;br /&gt;또한, 모든 트랜잭션 정의 설정이 모든 트랜잭션 매니저에서 지원되는 것은 아닙니다. 적절한 트랜잭션 매니저 구현체는 지원되지 않는 설정이 사용될 경우 예외를 발생시켜야 합니다.&lt;br /&gt;&lt;br /&gt;위 규칙의 예외는 read-only 플래그입니다. 명시적인 read-only 모드를 지원하지 않는 경우, 이 플래그는 무시되어야 합니다. 기본적으로 read-only 플래그는 잠재적인 최적화를 위한 힌트일 뿐입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 설명을 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여전히 비즈니스 로직에만 집중하는 것이 아닌 DB접근과 같은 다른 관심사의 업무를 수행해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❤️ 선언적 트랜잭션(@Transactional)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;개념&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transaction 관리를 별도의 어노테이션으로 분리하여 AOP로 횡단 관심사 처리를 진행하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 프로그래밍 과정에서 트랜잭션으로 인한 코드 중복 문제, 소스코드 유지 보수 어려움의 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;545&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSoxCF/dJMcahjfTaK/LKnWKitpj9O6XYsUlrKrSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSoxCF/dJMcahjfTaK/LKnWKitpj9O6XYsUlrKrSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSoxCF/dJMcahjfTaK/LKnWKitpj9O6XYsUlrKrSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSoxCF%2FdJMcahjfTaK%2FLKnWKitpj9O6XYsUlrKrSK%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;545&quot; height=&quot;82&quot; data-origin-width=&quot;545&quot; data-origin-height=&quot;82&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 작성하는 과정에서 @Transactional을 입력하려고 하면 매번 두 개의 import를 선택할 수 있음을 느꼈을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개는 무엇이 다르고 어떤 것을 선택해야 할까?&lt;/p&gt;
&lt;h4 data-end=&quot;186&quot; data-start=&quot;137&quot; data-ke-size=&quot;size20&quot;&gt;  Spring Transaction vs Jakarta Transaction 비교&lt;/h4&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100.698%;&quot; border=&quot;1&quot; data-end=&quot;1282&quot; data-start=&quot;188&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.6977%;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot;&gt;Spring Framework Transaction&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot;&gt;Jakarta Transcation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;451&quot; data-start=&quot;339&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;351&quot; data-start=&quot;339&quot;&gt;어노테이션 패키지&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;412&quot; data-start=&quot;351&quot; data-col-size=&quot;md&quot;&gt;org.springframework.transaction.annotation.Transactional&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;451&quot; data-start=&quot;412&quot; data-col-size=&quot;sm&quot;&gt;jakarta.transaction.Transactional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;495&quot; data-start=&quot;452&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;457&quot; data-start=&quot;452&quot;&gt;소속&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-col-size=&quot;md&quot; data-end=&quot;476&quot; data-start=&quot;457&quot;&gt;Spring Framework&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;495&quot; data-start=&quot;476&quot; data-col-size=&quot;sm&quot;&gt;Jakarta EE (표준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;556&quot; data-start=&quot;496&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;504&quot; data-start=&quot;496&quot;&gt;기반 기술&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;526&quot; data-start=&quot;504&quot; data-col-size=&quot;md&quot;&gt;Spring 자체 추상화 + AOP&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;556&quot; data-start=&quot;526&quot; data-col-size=&quot;sm&quot;&gt;JTA (Java Transaction API)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;626&quot; data-start=&quot;557&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;568&quot; data-start=&quot;557&quot;&gt;트랜잭션 매니저&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;599&quot; data-start=&quot;568&quot; data-col-size=&quot;md&quot;&gt;PlatformTransactionManager&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;626&quot; data-start=&quot;599&quot; data-col-size=&quot;sm&quot;&gt;JTA Transaction Manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;699&quot; data-start=&quot;627&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;638&quot; data-start=&quot;627&quot;&gt;기본 롤백 정책&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;667&quot; data-start=&quot;638&quot; data-col-size=&quot;md&quot;&gt;RuntimeException, Error 롤백&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;699&quot; data-start=&quot;667&quot; data-col-size=&quot;sm&quot;&gt;RuntimeException 롤백 (구현체 의존)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;766&quot; data-start=&quot;700&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;723&quot; data-start=&quot;700&quot;&gt;Checked Exception 롤백&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;740&quot; data-start=&quot;723&quot; data-col-size=&quot;md&quot;&gt;기본 ❌ (옵션으로 가능)&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;766&quot; data-start=&quot;740&quot; data-col-size=&quot;sm&quot;&gt;기본 ❌ (rollbackOn으로 지정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;852&quot; data-start=&quot;767&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;785&quot; data-start=&quot;767&quot;&gt;rollback 커스터마이징&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;818&quot; data-start=&quot;785&quot; data-col-size=&quot;md&quot;&gt;rollbackFor, noRollbackFor&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;852&quot; data-start=&quot;818&quot; data-col-size=&quot;sm&quot;&gt;rollbackOn, dontRollbackOn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;947&quot; data-start=&quot;853&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;874&quot; data-start=&quot;853&quot;&gt;전파 옵션(Propagation)&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;915&quot; data-start=&quot;874&quot; data-col-size=&quot;md&quot;&gt;REQUIRED, REQUIRES_NEW, NESTED 등 매우 다양&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;947&quot; data-start=&quot;915&quot; data-col-size=&quot;sm&quot;&gt;REQUIRED, REQUIRES_NEW 등 일부만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;990&quot; data-start=&quot;948&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;967&quot; data-start=&quot;948&quot;&gt;격리 수준(Isolation)&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;978&quot; data-start=&quot;967&quot; data-col-size=&quot;md&quot;&gt;직접 설정 가능&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;990&quot; data-start=&quot;978&quot; data-col-size=&quot;sm&quot;&gt;직접 설정 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1016&quot; data-start=&quot;991&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1004&quot; data-start=&quot;991&quot;&gt;timeout 설정&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;1009&quot; data-start=&quot;1004&quot; data-col-size=&quot;md&quot;&gt;가능&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;1016&quot; data-start=&quot;1009&quot; data-col-size=&quot;sm&quot;&gt;제한적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1051&quot; data-start=&quot;1017&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1031&quot; data-start=&quot;1017&quot;&gt;readOnly 옵션&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;1045&quot; data-start=&quot;1031&quot; data-col-size=&quot;md&quot;&gt;있음 (최적화 힌트)&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;1051&quot; data-start=&quot;1045&quot; data-col-size=&quot;sm&quot;&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1088&quot; data-start=&quot;1052&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1064&quot; data-start=&quot;1052&quot;&gt;AOP 기반 동작&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;1077&quot; data-start=&quot;1064&quot; data-col-size=&quot;md&quot;&gt;O (프록시 기반)&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;1088&quot; data-start=&quot;1077&quot; data-col-size=&quot;sm&quot;&gt;컨테이너 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1131&quot; data-start=&quot;1089&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1106&quot; data-start=&quot;1089&quot;&gt;ThreadLocal 사용&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;1121&quot; data-start=&quot;1106&quot; data-col-size=&quot;md&quot;&gt;사용 (동기 트랜잭션)&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;1131&quot; data-start=&quot;1121&quot; data-col-size=&quot;sm&quot;&gt;구현체 의존&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1186&quot; data-start=&quot;1132&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1146&quot; data-start=&quot;1132&quot;&gt;Reactive 지원&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;1181&quot; data-start=&quot;1146&quot; data-col-size=&quot;md&quot;&gt;O (ReactiveTransactionManager)&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;1186&quot; data-start=&quot;1181&quot; data-col-size=&quot;sm&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1254&quot; data-start=&quot;1187&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1197&quot; data-start=&quot;1187&quot;&gt;주 사용 환경&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;1225&quot; data-start=&quot;1197&quot; data-col-size=&quot;md&quot;&gt;Spring Boot / Spring 기반 앱&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;1254&quot; data-start=&quot;1225&quot; data-col-size=&quot;sm&quot;&gt;Jakarta EE 서버 (WildFly 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1282&quot; data-start=&quot;1255&quot;&gt;
&lt;td style=&quot;width: 20.6977%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1264&quot; data-start=&quot;1255&quot;&gt;세밀한 제어&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot; data-end=&quot;1272&quot; data-start=&quot;1264&quot; data-col-size=&quot;md&quot;&gt;매우 강함&lt;/td&gt;
&lt;td style=&quot;width: 32.2093%;&quot; data-end=&quot;1282&quot; data-start=&quot;1272&quot; data-col-size=&quot;sm&quot;&gt;비교적 단순&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring or Spring boot로 개발을 진행하는 환경이라면 Spring Framework Transaction을 선택하여 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;org.springframework.transaction.annotation.Transactional&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;클래스 주석 번역문&lt;/h4&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;개별 메서드 또는 클래스에 트랜잭션 속성을 설명합니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;이 어노테이션이 클래스 수준에 선언되면, 해당 클래스와 그 하위 클래스의 모든 메서드에 기본값으로 적용됩니다. 단, 클래스 계층 구조에서 상위(조상) 클래스에는 적용되지 않습니다. 상속된 메서드가 하위 클래스 수준의 어노테이션에 참여하려면 해당 메서드를 하위 클래스에서 다시 선언해야 합니다. 메서드 가시성 제약에 대한 자세한 내용은 레퍼런스 매뉴얼의 Transaction Management 섹션을 참고하십시오.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;이 어노테이션은 일반적으로 Spring의 org.springframework.transaction.interceptor.RuleBasedTransactionAttribute 클래스와 직접적으로 비교할 수 있습니다. 실제로 AnnotationTransactionAttributeSource는 이 어노테이션의 속성들을 RuleBasedTransactionAttribute의 속성으로 직접 변환합니다. 따라서 Spring의 트랜잭션 지원 코드는 어노테이션 자체를 알 필요가 없습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;속성 의미 (Attribute Semantics)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;이 어노테이션에 사용자 정의 롤백 규칙이 구성되지 않은 경우, 트랜잭션은 RuntimeException과 Error에 대해서는 롤백되지만, 체크 예외(checked exception)에 대해서는 롤백되지 않습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;롤백 규칙은 특정 예외가 발생했을 때 트랜잭션을 롤백할지 여부를 결정하며, 타입 또는 패턴을 기반으로 합니다. 사용자 정의 규칙은 rollbackFor / noRollbackFor 및 rollbackForClassName / noRollbackForClassName을 통해 설정할 수 있으며, 각각 타입 기반 또는 패턴 기반으로 규칙을 지정할 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;예외 타입을 사용하여 롤백 규칙을 정의한 경우(예: rollbackFor), 해당 타입은 발생한 예외의 타입과 비교하여 매칭됩니다. 구체적으로, 설정된 예외 타입 C가 있을 때, 발생한 예외 타입 T가 C와 동일하거나 C의 하위 클래스라면 매칭된 것으로 간주됩니다. 이는 타입 안정성을 제공하며, 패턴 사용 시 발생할 수 있는 의도치 않은 매칭을 방지합니다. 예를 들어 jakarta.servlet.ServletException.class를 지정하면, jakarta.servlet.ServletException 및 그 하위 클래스에 대해서만 매칭됩니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;예외 패턴을 사용하여 롤백 규칙을 정의한 경우, 패턴은 예외 타입(반드시 Throwable의 하위 클래스여야 함)의 전체 클래스 이름(FQCN)이거나 그 일부 문자열일 수 있습니다. 현재는 와일드카드 지원이 없습니다. 예를 들어 &quot;jakarta.servlet.ServletException&quot; 또는 &quot;ServletException&quot;이라는 값은 jakarta.servlet.ServletException 및 그 하위 클래스와 매칭됩니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;주의: 패턴을 얼마나 구체적으로 지정할지, 그리고 패키지 정보를 포함할지(필수는 아님)를 신중히 고려해야 합니다. 예를 들어 &quot;Exception&quot;은 거의 모든 예외와 매칭되므로 다른 규칙을 가릴 가능성이 큽니다. 만약 모든 체크 예외에 대한 규칙을 정의하려는 목적이라면 &quot;java.lang.Exception&quot;을 사용하는 것이 적절합니다. &quot;BaseBusinessException&quot;처럼 고유한 이름을 가진 예외라면 전체 클래스 이름을 사용할 필요가 없을 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;또한 패턴 기반으로 정의된 롤백 규칙은 이름이 유사한 예외나 중첩 클래스에 대해 의도치 않은 매칭을 일으킬 수 있습니다. 이는 발생한 예외의 이름에 설정된 예외 패턴 문자열이 포함되어 있으면 매칭된 것으로 간주되기 때문입니다. 예를 들어 &quot;com.example.CustomException&quot;이라는 규칙이 설정된 경우, 다음과 같은 예외에도 매칭됩니다:&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;com.example.CustomExceptionV2 (같은 패키지에 있으나 이름에 접미사가 추가된 경우) com.example.CustomException$AnotherException (CustomException 내부에 선언된 중첩 클래스)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;이 어노테이션의 다른 속성 의미에 대한 구체적인 정보는 TransactionDefinition 및 org.springframework.transaction.interceptor.TransactionAttribute의 Javadoc을 참고하십시오.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;트랜잭션 관리 (Transaction Management)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;이 어노테이션은 일반적으로 org.springframework.transaction.PlatformTransactionManager에 의해 관리되는 스레드 바인딩(thread-bound) 트랜잭션과 함께 동작하며, 현재 실행 스레드 내의 모든 데이터 접근 작업에 트랜잭션을 노출합니다. 단, 이 트랜잭션은 해당 메서드 내부에서 새롭게 시작된 스레드로는 전파되지 않습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;또는 이 어노테이션은 org.springframework.transaction.ReactiveTransactionManager에 의해 관리되는 리액티브 트랜잭션을 구분하는 데 사용될 수 있습니다. 이 경우 ThreadLocal 변수가 아니라 Reactor Context를 사용합니다. 따라서 모든 참여 데이터 접근 작업은 동일한 리액티브 파이프라인 내에서, 동일한 Reactor Context 안에서 실행되어야 합니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;참고: ReactiveTransactionManager와 함께 구성된 경우, 모든 트랜잭션이 선언된 메서드는 리액티브 파이프라인을 반환해야 합니다. void 메서드나 일반 반환 타입을 사용하는 경우에는 일반적인 PlatformTransactionManager와 연결되어야 하며, 예를 들어 transactionManager()를 통해 설정해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메서드&lt;/h4&gt;
&lt;pre id=&quot;code_1772418488303&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public @interface Transactional {

    // 트랜잭션 매니저 이름 지정 (transactionManager의 별칭)
    // 여러 TransactionManager가 있을 때 특정 매니저를 선택하기 위해 사용
    String value() default &quot;&quot;;

    // 사용할 PlatformTransactionManager Bean 이름 지정
    // 멀티 데이터소스 환경에서 사용
    String transactionManager() default &quot;&quot;;

    // 트랜잭션에 라벨(태그)을 부여
    // 모니터링/추적용 메타데이터이며 트랜잭션 동작에는 영향 없음
    String[] label() default {};

    // 트랜잭션 전파 옵션 설정
    // 기존 트랜잭션이 있을 때 어떻게 동작할지 결정 (기본: REQUIRED)
    Propagation propagation() default Propagation.REQUIRED;

    // 트랜잭션 격리 수준 설정
    // 동시성 제어를 위한 설정이며 기본값은 DB 기본값 사용
    Isolation isolation() default Isolation.DEFAULT;

    // 트랜잭션 타임아웃(초 단위)
    // 지정 시간 초과 시 롤백 (기본값은 트랜잭션 매니저 기본값 사용)
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    // 타임아웃을 문자열로 지정
    // SpEL 또는 프로퍼티 값 사용 가능
    String timeoutString() default &quot;&quot;;

    // 읽기 전용 트랜잭션 여부
    // 성능 최적화를 위한 힌트 (강제 제한은 아님)
    boolean readOnly() default false;

    // 지정한 예외 타입 발생 시 롤백
    // 해당 타입 및 하위 클래스까지 적용
    Class&amp;lt;? extends Throwable&amp;gt;[] rollbackFor() default {};

    // 예외 클래스 이름(문자열 패턴) 기반 롤백 지정
    // 이름에 해당 문자열이 포함되면 매칭됨 (와일드카드 없음)
    String[] rollbackForClassName() default {};

    // 지정한 예외 타입 발생 시 롤백하지 않음
    // RuntimeException이라도 커밋하도록 설정 가능
    Class&amp;lt;? extends Throwable&amp;gt;[] noRollbackFor() default {};

    // 예외 클래스 이름(문자열 패턴) 기반 롤백 제외 지정
    String[] noRollbackForClassName() default {};
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션 격리 수준 (Isolation Level)&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;동시에 실행되는 트랜잭션 간의 데이터 접근 허용 범위를 결정하는 설정&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;트랜잭션 이상 현상&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dirty Read&lt;/td&gt;
&lt;td&gt;커밋되지 않은 데이터를 읽음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-repeatable Read&lt;/td&gt;
&lt;td&gt;같은 데이터를 두 번 조회했는데 값이 달라짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phantom Read&lt;/td&gt;
&lt;td&gt;같은 조건으로 조회했는데 행 개수가 달라짐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. DEFAULT&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스에서 설정된 기본 격리 수준을 따름&lt;/li&gt;
&lt;li&gt;Spring은 관여하지 않음&lt;/li&gt;
&lt;li&gt;보통 DB 기본값 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL(InnoDB) 기본: REPEATABLE READ&lt;/li&gt;
&lt;li&gt;Oracle 기본: READ COMMITTED&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. READ_UNCOMMITTED&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커밋되지 않은 데이터 조회 가능&lt;/li&gt;
&lt;li&gt;Dirty Read 허용&lt;/li&gt;
&lt;li&gt;가장 낮은 격리 수준&lt;/li&gt;
&lt;li&gt;정합성 낮음, 동시성 높음&lt;/li&gt;
&lt;li&gt;대부분의 상용 DB에서는 사실상 READ_COMMITTED처럼 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. READ_COMMITTED&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커밋된 데이터만 조회 가능&lt;/li&gt;
&lt;li&gt;Dirty Read 방지&lt;/li&gt;
&lt;li&gt;Non-repeatable Read, Phantom Read는 발생 가능&lt;/li&gt;
&lt;li&gt;실무에서 가장 많이 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. REPEATABLE_READ&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일 트랜잭션 내에서 같은 데이터 조회 시 항상 동일 값 보장&lt;/li&gt;
&lt;li&gt;Dirty Read, Non-repeatable Read 방지&lt;/li&gt;
&lt;li&gt;Phantom Read는 DB 구현에 따라 발생 가능&lt;/li&gt;
&lt;li&gt;MySQL 기본 격리 수준&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. SERIALIZABLE&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 높은 격리 수준&lt;/li&gt;
&lt;li&gt;모든 트랜잭션을 순차 실행처럼 보장&lt;/li&gt;
&lt;li&gt;Dirty / Non-repeatable / Phantom Read 모두 방지&lt;/li&gt;
&lt;li&gt;동시성 매우 낮음&lt;/li&gt;
&lt;li&gt;성능 저하 가능성 큼&lt;/li&gt;
&lt;li&gt;금융/정산 등 강한 정합성이 필요한 경우 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Spring에서의 특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;isolation 옵션은 &quot;새로운 트랜잭션 생성 시에만&quot; 적용&lt;/li&gt;
&lt;li&gt;기존 트랜잭션에 참여하는 경우 무시됨&lt;/li&gt;
&lt;li&gt;DB가 지원하지 않는 격리 수준이면 예외 발생 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션 전파 옵션 (Propagation)&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기존 트랜잭션 존재 여부에 따라 현재 메서드가 어떻게 동작할지 결정&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. REQUIRED (기본값)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 트랜잭션이 있으면 참여&lt;/li&gt;
&lt;li&gt;없으면 새로 생성&lt;/li&gt;
&lt;li&gt;가장 많이 사용되는 옵션&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. SUPPORTS&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 트랜잭션이 있으면 참여&lt;/li&gt;
&lt;li&gt;없으면 트랜잭션 없이 실행&lt;/li&gt;
&lt;li&gt;조회용 메서드에 자주 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. REQUIRES_NEW&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;항상 새로운 트랜잭션 생성&lt;/li&gt;
&lt;li&gt;기존 트랜잭션은 잠시 중단(Suspend)&lt;/li&gt;
&lt;li&gt;독립적인 커밋/롤백 필요할 때 사용&lt;/li&gt;
&lt;li&gt;예: 로그 저장, 감사 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. MANDATORY&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 트랜잭션이 반드시 있어야 함&lt;/li&gt;
&lt;li&gt;없으면 예외 발생&lt;/li&gt;
&lt;li&gt;단독 실행이 허용되지 않는 경우 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. NOT_SUPPORTED&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 없이 실행&lt;/li&gt;
&lt;li&gt;기존 트랜잭션 있으면 중단&lt;/li&gt;
&lt;li&gt;락을 피하고 싶은 대량 조회 등에 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. NEVER&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션이 존재하면 예외 발생&lt;/li&gt;
&lt;li&gt;완전 비트랜잭션 환경 강제&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7. NESTED&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 트랜잭션 내부에 중첩 트랜잭션 생성&lt;/li&gt;
&lt;li&gt;Savepoint 기반 동작&lt;/li&gt;
&lt;li&gt;부모 롤백 시 자식도 롤백&lt;/li&gt;
&lt;li&gt;자식 롤백은 부모에 영향 없음&lt;/li&gt;
&lt;li&gt;DataSourceTransactionManager에서만 지원&lt;/li&gt;
&lt;li&gt;JPA는 기본적으로 지원하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@betterfuture4/Spring-Transactional-%EC%B4%9D%EC%A0%95%EB%A6%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@betterfuture4/Spring-Transactional-%EC%B4%9D%EC%A0%95%EB%A6%AC&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://adjh54.tistory.com/378&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://adjh54.tistory.com/378&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html#transaction-declarative-annotations-method-visibility&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html#transaction-declarative-annotations-method-visibility&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/Spring</category>
      <category>db</category>
      <category>Jakarta</category>
      <category>jpa</category>
      <category>mysql</category>
      <category>spring</category>
      <category>transaction</category>
      <category>transactional</category>
      <author>27200</author>
      <guid isPermaLink="true">https://to-travel-coding.tistory.com/472</guid>
      <comments>https://to-travel-coding.tistory.com/472#entry472comment</comments>
      <pubDate>Mon, 2 Mar 2026 11:38:21 +0900</pubDate>
    </item>
    <item>
      <title>[KT CLOUD TECH UP] 웹 백엔드 파트 1기 중간 후기</title>
      <link>https://to-travel-coding.tistory.com/471</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;1283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nvz7z/dJMcabwdOpn/6poKzliXSCW1pSHlKbguAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nvz7z/dJMcabwdOpn/6poKzliXSCW1pSHlKbguAk/img.png&quot; data-alt=&quot;https://ktcloud-techup.com/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nvz7z/dJMcabwdOpn/6poKzliXSCW1pSHlKbguAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnvz7z%2FdJMcabwdOpn%2F6poKzliXSCW1pSHlKbguAk%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;1192&quot; height=&quot;1283&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;1283&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://ktcloud-techup.com/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 교육과정은 KT CLOUD에서 직접 커리큘럼을 설계하고 제작하여 구름과 함께 진행하는 부트캠프이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 흥미로웠던 부분은 kt cloud 혹은 대기업의 이름을 걸고 진행하는 부트캠프는 되게 많이 보았지만, 실제로 운영에 관여 한다는 느낌을 받은 것은 처음이라 찾아보게 된 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=0xlwUnrinIM&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=0xlwUnrinIM&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=0xlwUnrinIM&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/9es7L/dJMb86nRO0f/I37op5YOWKUqckDo8WixV0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=740_180_960_420,https://scrap.kakaocdn.net/dn/Cs3EW/dJMb85vI8Cj/yo7Ejiqk3QZCv1ErkJCKEk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=740_180_960_420,https://scrap.kakaocdn.net/dn/cqo7Mz/dJMb83kneep/UPwOsOldRbJnQ6U2E8lO6k/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=740_180_960_420&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;&amp;lsquo;실무형 인재&amp;rsquo;는 무엇이 다른가? | kt cloud TECH UP&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/0xlwUnrinIM&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 해당 영상을 보게 되었고, 신뢰와 확신이 생겨 지원하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소 링크드인을 통해 통찰력이 높으신 분이라고 생각했는데, 이 분이 직접 영상을 통해 설명해주시니 믿음이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지원 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원은 총 3가지 방식이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 5분 셀프 인터뷰 제출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 프로젝트와 포트폴리오 제출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 면접&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대학교 친구들과 함께 1,2,3 모두 다양한 방식으로 지원해본 결과 쉽게 합격할 수 있었다. 아무래도 1기이다보니 관심도가 적었기에 그럴 수 있었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자의 경우 1번인 5분 영상을 제출했는데 색다른 방식이라 신기하면서도 어떤 말을 해야할지 고민되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으론, 면접을 위해 준비했던 1분 자기소개와 추가적으로 진행했던 프로젝트에 대한 설명으로 5분을 채워냈던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;합격&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 너무 쉽게 합격해버린 것 같은 느낌이 있어 이 과정을 시작하는게 맞을까 고민이 있었다.(합격생들의 수준이 높았으면 좋았기에 했던 생각이었다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=bo9IgI0sPpE&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=bo9IgI0sPpE&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=bo9IgI0sPpE&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bGAPR8/dJMb88eUTim/7LwNIMN4jLsKLvmrczscv0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=4_40_1034_362,https://scrap.kakaocdn.net/dn/A2zNF/dJMb84p22kd/LeUBOJJqgZFMKfduKb8zzK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=4_40_1034_362,https://scrap.kakaocdn.net/dn/iS33U/dJMb895XTuk/lurpUErIQIQIIgP8HcRwsk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=4_40_1034_362&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;학습을 넘어 실무형 훈련으로의 첫걸음  | kt cloud TECH UP&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/bo9IgI0sPpE&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 OT에 참석한 후 생각이 많이 바뀌었다.&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;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BXiv3/dJMcaaxkpjV/ViZtdIVSjNEEbXtKRiRrNk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BXiv3/dJMcaaxkpjV/ViZtdIVSjNEEbXtKRiRrNk/img.jpg&quot; data-alt=&quot;https://blog.goorm.io/wp-content/uploads/2022/11/%EA%B5%AC%EB%A6%84%EC%8A%A4%ED%80%98%EC%96%B4-2-scaled-1520x912.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BXiv3/dJMcaaxkpjV/ViZtdIVSjNEEbXtKRiRrNk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBXiv3%2FdJMcaaxkpjV%2FViZtdIVSjNEEbXtKRiRrNk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;768&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://blog.goorm.io/wp-content/uploads/2022/11/%EA%B5%AC%EB%A6%84%EC%8A%A4%ED%80%98%EC%96%B4-2-scaled-1520x912.jpg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KT Cloud와 구름의 대표이사님, 그리고 현업의 정점에 계신 멘토님들이 직접 참석하신 모습에서 이 과정에 실린 무게감을 느낄 수 있었다. 운영진 분들의 에너지가 느껴졌고, 챌린저들도 좋은 동기부여를 갖고 있다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 KT CLOUD 대표이사님과 구름 대표이사님을 포함한 엄청난 멘토님들이 오셨기에 꽤 값진 경험이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(OT 참여 선물로 비타민 두박스도 받아왔다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 프로젝트 &amp;amp; 심화 프로젝트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교육은 오전의 집중 강의와 오후의 개인 실습 및 팀 프로젝트로 밀도 있게 구성되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 초반 한 달간 제공된 인프런 무제한 수강권은 이전에 들어보고 싶었던 강의를 들어보고, 평소 학습하기에는 당장의 기술이 아니고 비용적인 면에서 고민됐던 강의들을 들을 수 있어서 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(개인적으로 가장 만족한 것은 조영호님의 오브젝트이다.)&lt;/p&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;필자의 경우 처음 접해보는 kafka 공부를 진행하며 채팅 시스템을 구현하는데 힘을 주었다. 분산 메시징 시스템의 원리를 이해하고, 이를 채팅 시스템에 녹여내며 실시간 데이터 처리에 대해 더욱 깊게 공부해보고 싶다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 좋은 결과물을 만들어 낼 수 있었고, 만족스러웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/kt-techup-shopping/shopping-management-system&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/kt-techup-shopping/shopping-management-system&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1769246051833&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - kt-techup-shopping/shopping-management-system: ShoppingManagementSystem&quot; data-og-description=&quot;ShoppingManagementSystem. Contribute to kt-techup-shopping/shopping-management-system development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/kt-techup-shopping/shopping-management-system&quot; data-og-url=&quot;https://github.com/kt-techup-shopping/shopping-management-system&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b9ecO9/dJMb8UHJtgJ/dKlBBZ8GZUTdLY3ODE8KLK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/I023x/dJMb9kl68iB/9RhVsHDzb4Aj1UO5ELbHIK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/kt-techup-shopping/shopping-management-system&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/kt-techup-shopping/shopping-management-system&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b9ecO9/dJMb8UHJtgJ/dKlBBZ8GZUTdLY3ODE8KLK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/I023x/dJMb9kl68iB/9RhVsHDzb4Aj1UO5ELbHIK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - kt-techup-shopping/shopping-management-system: ShoppingManagementSystem&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ShoppingManagementSystem. Contribute to kt-techup-shopping/shopping-management-system development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V0SB4/dJMcahDb0mY/7fFXHV9kl3fia3q5scQt41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V0SB4/dJMcahDb0mY/7fFXHV9kl3fia3q5scQt41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V0SB4/dJMcahDb0mY/7fFXHV9kl3fia3q5scQt41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV0SB4%2FdJMcahDb0mY%2F7fFXHV9kl3fia3q5scQt41%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;717&quot; height=&quot;387&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;387&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 자리를 빌어 팀원들에게 감사를 표하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 프로젝트&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;276&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brs1wW/dJMcadtZqk2/ogDGnsxIkS1TfwpwHjBGTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brs1wW/dJMcadtZqk2/ogDGnsxIkS1TfwpwHjBGTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brs1wW/dJMcadtZqk2/ogDGnsxIkS1TfwpwHjBGTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbrs1wW%2FdJMcadtZqk2%2FogDGnsxIkS1TfwpwHjBGTk%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;276&quot; height=&quot;456&quot; data-origin-width=&quot;276&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 프로젝트의 경우 총 4개의 그룹 &amp;amp; 15명의 팀원이 함께 프로젝트를 진행하고 있다.&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;물론 비대면이라는 환경 속에서 15명의 팀원이 모두 의견을 활발하게 공유하는 것은 조금 어렵다고 느껴진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소통의 병목이 크고, 의견 결정 과정에서 더 많은 시간을 쓰게 되는 것 같다. 하지만 긍정적으로 생각해보면 더 좋은 협업 경험이 될 수도 있다고 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 구름이 제공하는 공간을 활용한다면 대면으로도 만날 수 있어 아주 좋은 경험이 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 3개월의 시간동안 단순히 동작하는 코드가 아닌 하나된 팀으로서 좋은 결과물을 도출해보고 싶다.&lt;/p&gt;</description>
      <category>기록</category>
      <category>backend</category>
      <category>KT Cloud</category>
      <category>TECH UP</category>
      <author>27200</author>
      <guid isPermaLink="true">https://to-travel-coding.tistory.com/471</guid>
      <comments>https://to-travel-coding.tistory.com/471#entry471comment</comments>
      <pubDate>Sat, 24 Jan 2026 20:20:15 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Group, Topic, Record, Consumer, Partition 총 정리</title>
      <link>https://to-travel-coding.tistory.com/470</link>
      <description>&lt;h1&gt;&lt;b&gt;Record&lt;/b&gt;&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;531&quot; data-origin-height=&quot;391&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ryDXs/dJMcai205HJ/Pgf0kDXjjxXJHjHMZWqvkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ryDXs/dJMcai205HJ/Pgf0kDXjjxXJHjHMZWqvkK/img.png&quot; data-alt=&quot;https://devboi.tistory.com/659&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ryDXs/dJMcai205HJ/Pgf0kDXjjxXJHjHMZWqvkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FryDXs%2FdJMcai205HJ%2FPgf0kDXjjxXJHjHMZWqvkK%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;531&quot; height=&quot;391&quot; data-origin-width=&quot;531&quot; data-origin-height=&quot;391&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://devboi.tistory.com/659&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 레코드의 구조는 위와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서가 생성한 레코드가 브로커로 전송되면 offset &amp;amp; timestamp가 지정되어 저장된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;timestamp
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스트림 프로세싱에서의 활용을 위해 시간을 저장하는 용도로 사용된다.&lt;/li&gt;
&lt;li&gt;따로 설정하지 않으면 PrdocuerRecord의 생성시간이 들어간다.&lt;/li&gt;
&lt;li&gt;적재시간으로 변경할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;offset
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로듀서가 생성한 레코드에 존재하는 것은 아니다.&lt;/li&gt;
&lt;li&gt;브로커에 적재될 때 오프셋이 지정된다.(0-based idx)&lt;/li&gt;
&lt;li&gt;컨슈머는 오프셋을 기반으로 처리가 완료된 데이터와 처리하지 못한 데이터를 구분한다.&lt;/li&gt;
&lt;li&gt;파티션별 고유한 오프셋을 가지므로, 컨슈머에서 중복 처리를 방지하기 위해서도 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;headers
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key/value로 추가 가능하며 데이터를 넣어 사용 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;key
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처리하고자하는 메시지 값의 분류 용도로 사용된다. 이를 파티셔닝이라고 부른다.&lt;/li&gt;
&lt;li&gt;파티셔너에 따라 토픽의 파티션 번호가 정해진다.&lt;/li&gt;
&lt;li&gt;필수 값이 아니며, 지정하지 않을 시 NULL로 설정된다.&lt;/li&gt;
&lt;li&gt;메시지 키가 Null인 레코드는 토픽의 파티션에 라운드로빈으로 전달된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2.4 버전 이후 매버 라운드로빈이 작동하는 것이 아닌 일정량이 채워지면 다른 파티션으로 이동하는 Sticky 방식을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;value
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 처리가 될 데이터가 저장되는 공간&lt;/li&gt;
&lt;li&gt;제네릭으로 사용자에 의해 지정되며, 컨슈머가 역직렬화 포맷을 알고 있어야 한다.&lt;/li&gt;
&lt;li&gt;대부분 String으로 처리하고, 공간 낭비가 심한 경우 다른 형태로 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 레코드가 실제로 하나로 존재하는 것은 아니고, ProducerRecord와 ConsumerRecord로 나누어져 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ProducerRecord&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class ProducerRecord&amp;lt;K, V&amp;gt; {

    private final String topic;
    private final Integer partition;
    private final Headers headers;
    private final K key;
    private final V value;
    private final Long timestamp;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;topic
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;record가 어느 topic으로 전송되어야하는지를 저장하는 값이며, 레코드 생성시 초기화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;public class ConsumerRecord&amp;lt;K, V&amp;gt; {
    public static final long NO_TIMESTAMP = RecordBatch.NO_TIMESTAMP;
    public static final int NULL_SIZE = -1;

    /**
     * @deprecated checksums are no longer exposed by this class, this constant will be removed in Apache Kafka 4.0
     *             (deprecated since 3.0).
     */
    @Deprecated
    public static final int NULL_CHECKSUM = -1;

    private final String topic;
    private final int partition;
    private final long offset;
    private final long timestamp;
    private final TimestampType timestampType;
    private final int serializedKeySize;
    private final int serializedValueSize;
    private final Headers headers;
    private final K key;
    private final V value;
    private final Optional&amp;lt;Integer&amp;gt; leaderEpoch;&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;serializedKeySize &amp;amp; serializedValueSize
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;producer record 단에서 직렬화한 key/value의 크기를 저장해놓은 값이다. 역직렬화시 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;leader epoch
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;leader epoch은 partition의 leader가 변경횟수를 기록하는 값으로 leader가 변경될 때마다 증가하는 값이다. 컨슈머는 이를 통해 리더가 언제 변경되었는지를 파악할 수 있으며, offset을 재조정하거나 데이터를 재처리해야 하는 시점을 판단할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring에서의 실제 사용&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
public CompletableFuture&amp;lt;SendResult&amp;lt;K, V&amp;gt;&amp;gt; send(String topic, K key, @Nullable V data) {
    ProducerRecord&amp;lt;K, V&amp;gt; producerRecord = new ProducerRecord&amp;lt;&amp;gt;(topic, key, data);
    return observeSend(producerRecord);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaTemplate의 send메서드를 보면 실제 ProducerRecord를 만들어서 메세지를 전송하는 것을 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class KafkaMessageListenerContainer&amp;lt;K, V&amp;gt;{ // NOSONAR line count
        ...
        private final BlockingQueue&amp;lt;ConsumerRecord&amp;lt;K, V&amp;gt;&amp;gt; acks = new LinkedBlockingQueue&amp;lt;&amp;gt;();
        ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 @KafkaListener 어노테이션을 통해 처리하기 때문에 직접적으로 명시되어있지는 않지만 내부 과정을 살펴보면 다음과 같이 ConsumerRecord가 큐에 들어가있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;b&gt;Topic &amp;amp; Partition&lt;/b&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Topic?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 스트림을 카프카에서는 토픽이라고 부른다. 카프카 세계에선 토픽이 구체화된 이벤트 스트림을 뜻한다. 이는 데이터 베이스의 테이블과 파일 시스템의 폴더와 유사하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽은 카프카에서 Producer/Consumer을 구분할 수 있게 한다. Producer은 카프카의 토픽에 메시지를 저장하고, Consumer은 저장된 메시지를 읽어온다. 즉, 하나의 토픽에 대해 여러 Producer/Comsumer가 존재할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0tLN6/dJMcagRImrG/n170zZtrW0Roo5wUWOpxok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0tLN6/dJMcagRImrG/n170zZtrW0Roo5wUWOpxok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0tLN6/dJMcagRImrG/n170zZtrW0Roo5wUWOpxok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0tLN6%2FdJMcagRImrG%2Fn170zZtrW0Roo5wUWOpxok%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;686&quot; height=&quot;377&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;377&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 모든 토픽은 여러개의 Partition을 갖게 된다. 이는 사용자가 설정을 하거나, 기본값으로 지정되어 생성될 수 있다. 0-based idx로 시작한다.&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;public static class CreatableTopic implements Message, ImplicitLinkedHashMultiCollection.Element {
        String name;
        int numPartitions; // 파티션 수 지정
        short replicationFactor;
        CreatableReplicaAssignmentCollection assignments;
        CreatableTopicConfigCollection configs;
        private List&amp;lt;RawTaggedField&amp;gt; _unknownTaggedFields;
        private int next;
        private int prev;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 알 수 있듯이 파티션은 토픽을 구성하는 요소이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Partition&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽의 구성사항을 담당하는 것이 파티션이다. 즉, 토픽은 논리적인 개념에 가깝고, 파티션이 레코드를 실제 저장소에 저장하는 가장 작은 단위이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 파티션은 Append-Only 방식으로 기록되는 하나의 로그 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 왜 Appen-Only인지 궁금할 수 있지만 이는 카프카가 offset을 이용해 정보를 관리할 뿐, 소멸시키지 않는다는 것과 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션의 레코드는 각각이 Offset라 불리는 식별자 정보를 가지며, 이를 사용해 순서를 보장한다. 다만 모순적이게도 순서가 보장되지 않기도 한다. &amp;rarr; 파티션 내부의 순서는 보장되더라도 파티션 간의 순서가 보장되지 않을 수 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 카프카가 흔히 순서가 보장된다 라고 하는 것은 하나의 컨슈머는 하나의 파티션에 붙어있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;파티션은 카프카가 병렬 처리, 순서 보장, 확장성의 장점을 제공하게 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떻게?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;병렬 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 파티션 별로 컨슈머가 존재하기 때문에 여러개의 파티션에 대해 컨슈머가 각각 처리하게 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;순서 보장
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 언급한 것과 같이 offset을 통해 처리 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;확장성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 하나의 파티션만 존재했다면 컨슈머가 추가되는 것이 매우 어려울 것이다. 하지만, 파티션이 존재한다면 파티션이 추가됨에 따라 컨슈머가 추가되면 손쉽게 확장 가능하다.&lt;/li&gt;
&lt;li&gt;하나의 파티션만 존재한다면 토픽의 확장성은 브로커의 I/O 처리량에 의해 제약된다. 파티션들을 여러 브로커에 나눔으로써, 하나의 토픽은 수평적으로 확장될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그럼 파티션 분배는?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서가 데이터를 파티션에 할당할 때 키를 통해 파티션을 분배한다. 들어온 키를 해시함수를 통해 파티션을 설정하게되는 것이다. 하지만 이는 특정 파티션에 부하가 쏠리는 경우가 발생할 수 있다. 이를 대비하기 위해 키의 해시 분포가 균일하도록 키 선택 전략을 명확하게 정의해야한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rBMBc/dJMcahXpQaI/cbQmYQ8G3g02iBMQQ8QXsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rBMBc/dJMcahXpQaI/cbQmYQ8G3g02iBMQQ8QXsk/img.png&quot; data-alt=&quot;균형잡힌 분산&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rBMBc/dJMcahXpQaI/cbQmYQ8G3g02iBMQQ8QXsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrBMBc%2FdJMcahXpQaI%2FcbQmYQ8G3g02iBMQQ8QXsk%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;1194&quot; height=&quot;926&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;균형잡힌 분산&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5M3j3/dJMcacBMYt3/yjcSRgMjkferc40mbxL2F1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5M3j3/dJMcacBMYt3/yjcSRgMjkferc40mbxL2F1/img.png&quot; data-alt=&quot;불균형한 분산&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5M3j3/dJMcacBMYt3/yjcSRgMjkferc40mbxL2F1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5M3j3%2FdJMcacBMYt3%2FyjcSRgMjkferc40mbxL2F1%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;1188&quot; height=&quot;922&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;불균형한 분산&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Consumer &amp;amp; Group&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Consumer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카의 최말단에 있는 컨슈머이다. 컨슈머는 컨슈머 API와 애플리케이션을 통칭하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 컨슈머는 대표적인 3가지 특징을 갖는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;polling 구조&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 다른 메시징 큐는 메시지를 브로커가 푸시해준다. 이 방식의 가장 큰 단점은 메시지 큐가 컨슈머 측(서버 측)의 성능을 고려해야 한다는 것이다. 즉, 아키텍처를 설계하고 확장하는데 있어 양측 모두에 대한 고려가 직접적으로 필요하다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 반대로 &lt;span style=&quot;background-color: #f3c000;&quot;&gt;카프카는 컨슈머가 브로커로부터 메세지를 요청하는 Polling 구조로 설계&lt;/span&gt;되어있다. 즉, 컨슈머가 자신이 처리 가능한만큼만 브로커에게 요청할 수 있다. 이를 통해 환경을 최적화할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/pubsub/&quot;&gt;https://redis.io/docs/latest/develop/pubsub/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767610493884&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;Redis Pub/sub&quot; data-og-description=&quot;How to use pub/sub channels in Redis&quot; data-og-host=&quot;redis.io&quot; data-og-source-url=&quot;https://redis.io/docs/latest/develop/pubsub/&quot; data-og-url=&quot;https://redis.io/docs/latest/develop/pubsub/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/pubsub/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://redis.io/docs/latest/develop/pubsub/&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;Redis Pub/sub&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;How to use pub/sub channels in Redis&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;redis.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.rabbitmq.com/docs/consumers&quot;&gt;https://www.rabbitmq.com/docs/consumers&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767610490647&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;Consumers | RabbitMQ&quot; data-og-description=&quot;&amp;lt;!--&quot; data-og-host=&quot;www.rabbitmq.com&quot; data-og-source-url=&quot;https://www.rabbitmq.com/docs/consumers&quot; data-og-url=&quot;https://www.rabbitmq.com/docs/consumers&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.rabbitmq.com/docs/consumers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.rabbitmq.com/docs/consumers&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;Consumers | RabbitMQ&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt;!--&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.rabbitmq.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&lt;b&gt;단일 토픽의 다중 컨슈밍&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;421&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/opR47/dJMcagqDW6E/spj9TsEkx5TmkC9hiS9cRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/opR47/dJMcagqDW6E/spj9TsEkx5TmkC9hiS9cRK/img.png&quot; data-alt=&quot;https://www.linkedin.com/pulse/kafka-consumer-overview-sylvester-daniel&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/opR47/dJMcagqDW6E/spj9TsEkx5TmkC9hiS9cRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FopR47%2FdJMcagqDW6E%2Fspj9TsEkx5TmkC9hiS9cRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;954&quot; height=&quot;421&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;421&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.linkedin.com/pulse/kafka-consumer-overview-sylvester-daniel&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카 컨슈밍의 또 다른 중요한 특징 중 하나는 &lt;span style=&quot;background-color: #f3c000;&quot;&gt;하나의 토픽을 여러 개의 서로 다른 컨슈머 애플리케이션이 동시에 구독&lt;/span&gt;할 수 있다는 점이다. 예를 들어 하나의 토픽(Topic A)을 컨슈머 App 1과 컨슈머 App 2가 동시에 구독할 수 있다. 이때 각 컨슈머 애플리케이션은 서로 독립적으로 동작하며, 동일한 토픽의 메시지를 각자의 목적에 맞게 소비한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 단일 토픽에 대해 멀티 컨슈밍이 가능한 이유는, 컨슈머가 메시지를 읽더라도 브로커에 저장된 메시지가 삭제되지 않기 때문이다. Kafka에서 메시지는 큐 방식이 아니라 로그(log) 형태로 저장되며, 컨슈머의 소비 여부와 관계없이 설정된 보존 정책(retention policy)에 따라 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 Kafka는 각 컨슈머가 어느 토픽의 어떤 파티션에서, 어느 오프셋까지 메시지를 읽었는지를 별도로 관리한다. 이 정보는 컨슈머 오프셋(consumer offset) 이라 불리며, Kafka 내부에 존재하는 특수한 토픽인 __consumer_offsets에 저장된다. 컨슈머 오프셋은 컨슈머 그룹 단위로 관리되기 때문에, 서로 다른 컨슈머 애플리케이션 또는 서로 다른 컨슈머 그룹은 동일한 메시지에 대해서도 각자 독립적인 소비 위치를 가질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 오프셋을 통해 얻을 수 있는 장점은 멀티 컨슈밍 지원에만 국한되지 않는다. 컨슈머 애플리케이션이 메시지를 구독하던 중 중단되었다가 다시 실행되는 경우에도, __consumer_offsets 토픽에 저장된 오프셋 정보를 기반으로 이전에 처리하던 지점부터 메시지 소비를 재개할 수 있다. 이로 인해 메시지 유실 없이 안정적인 재처리가 가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 Kafka는 컨슈머의 실행 상태와 관계없이 메시지 소비 위치를 브로커 측에서 안정적으로 관리할 수 있으며, 이를 통해 대규모 분산 환경에서도 신뢰성 높은 메시지 구독과 처리가 가능하다. 이러한 구조는 Kafka가 로그 수집, 이벤트 스트리밍, 데이터 파이프라인 등 다양한 실시간 데이터 처리 시스템에서 핵심 인프라로 사용되는 중요한 이유 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.&lt;b&gt; 컨슈머 그룹&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BB8lu/dJMcabpkPe4/ouVt1YkKpI3y22YAbQFxGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BB8lu/dJMcabpkPe4/ouVt1YkKpI3y22YAbQFxGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BB8lu/dJMcabpkPe4/ouVt1YkKpI3y22YAbQFxGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBB8lu%2FdJMcabpkPe4%2FouVt1YkKpI3y22YAbQFxGk%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;953&quot; height=&quot;407&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 브로커는 높은 처리 성능을 위해 하나의 토픽을 여러 개의 파티션으로 분할하여 병렬로 처리한다. 각 파티션은 독립적으로 읽을 수 있기 때문에, 이를 적절히 분산 처리하면 전체 메시지 처리량을 크게 향상시킬 수 있다. 그러나 여러 개의 파티션을 단 하나의 컨슈머가 모두 처리하도록 구성할 경우, 컨슈머의 처리 속도가 병목이 되어 성능 저하가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 &lt;span style=&quot;background-color: #f3c000;&quot;&gt;Kafka는 &lt;b&gt;컨슈머 그룹(Consumer Group)&lt;/b&gt; 이라는 개념을 제공&lt;/span&gt;한다. 컨슈머 그룹은 하나 이상의 컨슈머가 논리적으로 하나의 그룹을 이루어 동일한 토픽을 구독하는 구조이다. 컨슈머 그룹을 사용하면 토픽의 여러 파티션을 그룹 내 컨슈머들 간에 분산하여 병렬로 처리할 수 있으며, 이를 통해 처리 성능과 확장성을 동시에 확보할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 그룹에 속한 각 컨슈머는 토픽 파티션의 &lt;b&gt;소유권(ownership)&lt;/b&gt; 을 나누어 가진다. 예를 들어, 파티션이 3개로 구성된 토픽 A를 2개의 컨슈머가 하나의 컨슈머 그룹으로 구독하는 경우를 생각해 볼 수 있다. 이때 컨슈머 0은 파티션 0의 소유권을 가지고 해당 파티션의 메시지를 소비한다. 반면 컨슈머 1은 파티션 1과 2의 소유권을 가지며, 이 두 파티션의 메시지를 소비한다. 이처럼 동일한 컨슈머 그룹에 속한 컨슈머들은 자신에게 할당된 파티션에 대해서만 메시지를 읽는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 컨슈머 그룹에 새로운 컨슈머가 추가되거나, 기존 컨슈머가 장애나 종료로 인해 그룹을 이탈하게 되면 어떻게 될까? 이 경우 컨슈머 그룹 내부에서는 &lt;b&gt;파티션 소유권을 다시 분배하는 과정&lt;/b&gt;이 발생한다. 이러한 파티션 소유권 재조정을 &lt;b&gt;리밸런싱(Rebalancing)&lt;/b&gt; 이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리밸런싱은 컨슈머 그룹 내 일부 컨슈머의 상태가 변경되더라도, 전체 그룹이 지속적으로 토픽을 안정적으로 구독할 수 있도록 보장하는 메커니즘이다. 이를 통해 컨슈머의 증감이나 장애 상황에서도 메시지 소비가 중단되지 않고 유연하게 이어질 수 있다. 다만 리밸런싱 과정은 내부 동작 방식과 설정에 따라 성능에 영향을 줄 수 있는 요소이므로, 이에 대한 자세한 내용은 별도의 글에서 다루는 것이 적절하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devboi.tistory.com/659&quot;&gt;https://devboi.tistory.com/659&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://zzzzseong.tistory.com/107&quot;&gt;https://zzzzseong.tistory.com/107&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://curiousjinan.tistory.com/entry/understand-kafka-partitions&quot;&gt;https://curiousjinan.tistory.com/entry/understand-kafka-partitions&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dkswnkk.tistory.com/736&quot;&gt;https://dkswnkk.tistory.com/736&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ggop-n.tistory.com/89&quot;&gt;https://ggop-n.tistory.com/89&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://always-kimkim.tistory.com/entry/kafka101-consumer&quot;&gt;https://always-kimkim.tistory.com/entry/kafka101-consumer&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kafka.apache.org/41/getting-started/introduction/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kafka.apache.org/41/getting-started/introduction/&lt;/a&gt;&lt;/p&gt;</description>
      <category>카프카</category>
      <category>group</category>
      <category>groupId</category>
      <category>kafka</category>
      <category>OFFSET</category>
      <category>Partition</category>
      <category>record</category>
      <category>topic</category>
      <author>27200</author>
      <guid isPermaLink="true">https://to-travel-coding.tistory.com/470</guid>
      <comments>https://to-travel-coding.tistory.com/470#entry470comment</comments>
      <pubDate>Mon, 5 Jan 2026 19:58:39 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 5557번. 1학년(JAVA)</title>
      <link>https://to-travel-coding.tistory.com/469</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/5557&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/5557&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;풀이(18분)&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1764637949798&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.*;
import java.util.*;

public class Main {

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st;

		int N = Integer.parseInt(br.readLine());
		int[] nums = new int[N-1]; // N-1개의 숫자를 저장

		st = new StringTokenizer(br.readLine());
		for(int i = 0; i &amp;lt; N-1; i++){
			nums[i] = Integer.parseInt(st.nextToken());
		}

		int target = Integer.parseInt(st.nextToken()); // 최종 목표 숫자

		long[][] answers = new long[N-1][21]; // 0 ~ 20 경우의 수 저장

		if(nums[0] - nums[1] &amp;gt;= 0){
			answers[1][nums[0] - nums[1]] += 1; // idx = 1 초기값
		}
		if(nums[0] + nums[1] &amp;lt;= 20){
			answers[1][nums[0] + nums[1]] += 1; // idx = 1 초기값
		}
		for(int i = 2; i &amp;lt; N-1; i++){
			int next = nums[i];
			for(int j = 0; j &amp;lt;= 20; j++){
				if(answers[i-1][j] == 0){ // 이전 값이 없다면 pass
					continue;
				}
				if(j - next &amp;lt; 0){ // 차가 0보다 작아지는 경우
					answers[i][j+next] += answers[i-1][j]; // 합만
					continue;
				}
				if(j + next &amp;gt; 20){ // 차가 20보다 커지는 경우
					answers[i][j-next] += answers[i-1][j]; // 차만
					continue;
				}
				// 아닌 경우 모두
				// 합 or 차만 했어도 되는 이유는
				// 문제에서 N의 범위가 0~9라고 한정되어있기 떄문.
				answers[i][j+next] += answers[i-1][j];
				answers[i][j-next] += answers[i-1][j];
			}
		}

		// idx = N-2에서 정답 출력
		System.out.println(answers[N-2][target]);

	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 풀이 전략&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;133&quot; data-start=&quot;103&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 단순 완전 탐색은 불가능하다는 결론&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;301&quot; data-start=&quot;134&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;200&quot; data-start=&quot;134&quot;&gt;각 숫자마다 + 또는 - 두 선택지를 가지므로&lt;br /&gt;&lt;b&gt;최악의 경우 2^98가지 경로&lt;/b&gt;가 나올 수 있음.&lt;/li&gt;
&lt;li data-end=&quot;301&quot; data-start=&quot;201&quot;&gt;문제에서 정답이 &lt;b&gt;최대 2^63 - 1&lt;/b&gt;까지 가능하다고 명시하는 것을 보아도,&lt;br /&gt;&lt;b&gt;모든 경우를 직접 탐색하는 방식은 절대 불가능&lt;/b&gt;하다는 것을 단번에 알 수 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;335&quot; data-start=&quot;308&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;335&quot; data-start=&quot;308&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 플로이드-워셜과 비슷한 구조?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;600&quot; data-start=&quot;336&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;448&quot; data-start=&quot;336&quot;&gt;숫자를 순서대로 더하거나 빼며 경로를 따라간다는 점이&lt;br /&gt;&quot;어떤 점을 거쳐 다음 점으로 이동한다&quot;는 &lt;b&gt;DP 경로 누적 구조&lt;/b&gt;와 닮아 있어&lt;br /&gt;순간적으로 플로이드-워셜 같은 느낌을 받았다.&lt;/li&gt;
&lt;li data-end=&quot;553&quot; data-start=&quot;449&quot;&gt;하지만 플로이드-워셜은 &lt;b&gt;모든 정점 간 최단 경로&lt;/b&gt;를 계산하는 알고리즘으로&lt;br /&gt;이 문제처럼 &lt;b&gt;+와 - 두 연산의 경로 선택만 존재하는 단방향 DP 구조&lt;/b&gt;와는 성격이 달랐다.&lt;/li&gt;
&lt;li data-end=&quot;600&quot; data-start=&quot;554&quot;&gt;즉, 경로 누적이라는 개념만 비슷할 뿐, 알고리즘 형태는 전혀 맞지 않았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;640&quot; data-start=&quot;607&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;640&quot; data-start=&quot;607&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 핵심 깨달음 &amp;mdash; 값의 범위는 단 0~20&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;864&quot; data-start=&quot;641&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;681&quot; data-start=&quot;641&quot;&gt;문제에서 중간 결과는 반드시 &lt;b&gt;0~20 범위 안&lt;/b&gt;에 있어야 한다.&lt;/li&gt;
&lt;li data-end=&quot;736&quot; data-start=&quot;682&quot;&gt;즉, N이 100에 달해도, &lt;b&gt;각 단계에서 고려해야 하는 값의 경우의 수는 21개뿐&lt;/b&gt;이다.&lt;/li&gt;
&lt;li data-end=&quot;787&quot; data-start=&quot;737&quot;&gt;그러면 &amp;ldquo;모든 단계 &amp;times; 가능한 값(0~20)&amp;rdquo;을 DP로 채우면 된다는 결론에 도달했다.&lt;/li&gt;
&lt;li data-end=&quot;864&quot; data-start=&quot;788&quot;&gt;이후 각 단계에서 가능한 값들을 업데이트하는 방식으로&lt;br /&gt;&lt;b&gt;완전 탐색이 아닌 DP로 충분히 해결 가능&lt;/b&gt;하다는 방향성을 잡았다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>문제 풀이/백준</category>
      <category>1학년</category>
      <category>5557</category>
      <category>dp</category>
      <category>java</category>
      <category>백준</category>
      <category>알고리즘</category>
      <category>자바</category>
      <author>27200</author>
      <guid isPermaLink="true">https://to-travel-coding.tistory.com/469</guid>
      <comments>https://to-travel-coding.tistory.com/469#entry469comment</comments>
      <pubDate>Tue, 2 Dec 2025 10:18:44 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] Boolean vs Enum</title>
      <link>https://to-travel-coding.tistory.com/468</link>
      <description>&lt;h1 data-end=&quot;89&quot; data-start=&quot;56&quot;&gt;MySQL에서 Boolean과 Enum 사용에 대한 고찰&lt;/h1&gt;
&lt;h2 data-end=&quot;104&quot; data-start=&quot;91&quot; data-ke-size=&quot;size26&quot;&gt;Boolean 타입&lt;/h2&gt;
&lt;p data-end=&quot;145&quot; data-start=&quot;106&quot; data-ke-size=&quot;size16&quot;&gt;MySQL에는 BOOLEAN이라는 타입 자체가 존재하지 않는다.&lt;/p&gt;
&lt;blockquote data-end=&quot;254&quot; data-start=&quot;146&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;254&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/data-types.html?utm_source=chatgpt.com&quot; data-end=&quot;254&quot; data-start=&quot;148&quot;&gt;MySQL 공식 문서 - Data Types&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure id=&quot;og_1763947679376&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;MySQL :: MySQL 8.4 Reference Manual :: 13 Data Types&quot; data-og-description=&quot;MySQL supports SQL data types in several categories: numeric types, date and time types, string (character and byte) types, spatial types, and the JSON data type. This chapter provides an overview and more detailed description of the properties of the type&quot; data-og-host=&quot;dev.mysql.com&quot; data-og-source-url=&quot;https://dev.mysql.com/doc/refman/8.4/en/data-types.html?utm_source=chatgpt.com&quot; data-og-url=&quot;https://dev.mysql.com/doc/refman/8.4/en/data-types.html?utm_source=chatgpt.com&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/data-types.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev.mysql.com/doc/refman/8.4/en/data-types.html?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MySQL :: MySQL 8.4 Reference Manual :: 13 Data Types&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;MySQL supports SQL data types in several categories: numeric types, date and time types, string (character and byte) types, spatial types, and the JSON data type. This chapter provides an overview and more detailed description of the properties of the type&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev.mysql.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;343&quot; data-start=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 MySQL에서 Boolean을 어떻게 사용할 수 있을까?&lt;br /&gt;MySQL은 &lt;b&gt;0과 1&lt;/b&gt;이라는 숫자를 Boolean처럼 처리할 수 있도록 지원한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;367&quot; data-start=&quot;350&quot; data-ke-size=&quot;size26&quot;&gt;BIT vs TINYINT&lt;/h2&gt;
&lt;p data-end=&quot;461&quot; data-start=&quot;369&quot; data-ke-size=&quot;size16&quot;&gt;Boolean 값을 저장하기 위해 BIT와 TINYINT를 선택할 수 있지만, MySQL 공식 문서와 실무 경험에 따르면 &lt;b&gt;TINYINT(1)&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;1454&quot; data-origin-height=&quot;1732&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZeaGc/dJMcafyfUyU/VxKU1mK4ITwyxkZkNIESW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZeaGc/dJMcafyfUyU/VxKU1mK4ITwyxkZkNIESW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZeaGc/dJMcafyfUyU/VxKU1mK4ITwyxkZkNIESW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZeaGc%2FdJMcafyfUyU%2FVxKU1mK4ITwyxkZkNIESW0%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;357&quot; height=&quot;425&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;1732&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgaC5U/dJMcadtCYl5/kjDkQS7ihbiDgGeCRg5SvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgaC5U/dJMcadtCYl5/kjDkQS7ihbiDgGeCRg5SvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgaC5U/dJMcadtCYl5/kjDkQS7ihbiDgGeCRg5SvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgaC5U%2FdJMcadtCYl5%2FkjDkQS7ihbiDgGeCRg5SvK%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;352&quot; height=&quot;301&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-end=&quot;473&quot; data-start=&quot;463&quot; data-ke-size=&quot;size23&quot;&gt;1. 가독성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;491&quot; data-start=&quot;475&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;491&quot; data-start=&quot;475&quot;&gt;&lt;b&gt;TINYINT 조회&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;*&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;/span&gt;&lt;span&gt; test_tinyint &lt;/span&gt;&lt;span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;/span&gt;&lt;span&gt; is_active &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;564&quot; data-start=&quot;552&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;564&quot; data-start=&quot;552&quot;&gt;&lt;b&gt;BIT 조회&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;*&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;/span&gt;&lt;span&gt; test_bit &lt;/span&gt;&lt;span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;/span&gt;&lt;span&gt; is_active &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span&gt; b&lt;/span&gt;&lt;span&gt;&lt;span&gt;'1'&lt;/span&gt;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;*&lt;/span&gt;&lt;/span&gt;&lt;span&gt;, is_active &lt;/span&gt;&lt;span&gt;&lt;span&gt;+&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;/span&gt;&lt;span&gt; test_bit; &lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-end=&quot;703&quot; data-start=&quot;676&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;703&quot; data-start=&quot;678&quot; data-ke-size=&quot;size16&quot;&gt;BIT 컬럼은 숫자로 변환해야 읽기가 편하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-end=&quot;714&quot; data-start=&quot;705&quot; data-ke-size=&quot;size23&quot;&gt;2. 성능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;755&quot; data-start=&quot;716&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;755&quot; data-start=&quot;716&quot;&gt;일반적으로 TINYINT가 인덱싱 및 조회 시 BIT보다 유리하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;776&quot; data-start=&quot;757&quot; data-ke-size=&quot;size23&quot;&gt;3. 프로그래밍 언어 호환성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;853&quot; data-start=&quot;778&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;815&quot; data-start=&quot;778&quot;&gt;BIT 컬럼은 일부 언어에서 변환 과정을 거쳐야 할 수도 있다.&lt;/li&gt;
&lt;li data-end=&quot;853&quot; data-start=&quot;816&quot;&gt;TINYINT는 대부분의 언어에서 Boolean과 호환이 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;867&quot; data-start=&quot;855&quot; data-ke-size=&quot;size23&quot;&gt;4. 저장 공간&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;905&quot; data-start=&quot;869&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;905&quot; data-start=&quot;869&quot;&gt;일반적인 상식과 달리 BIT(1)과 TINYINT(1)의 저장공간은 차이가 없다.&lt;/li&gt;
&lt;li data-end=&quot;905&quot; data-start=&quot;869&quot;&gt;BIT(1)와 TINYINT(1)은 모두 1바이트를 사용한다.&lt;/li&gt;
&lt;li data-end=&quot;905&quot; data-start=&quot;869&quot;&gt;아래 사진과 같이 BIT(1) 또한 (1+7)/8 바이트를 사용하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;1328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l5x6l/dJMcabikiKu/4h1zcApYyVb1rQJcpDGmv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l5x6l/dJMcabikiKu/4h1zcApYyVb1rQJcpDGmv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l5x6l/dJMcabikiKu/4h1zcApYyVb1rQJcpDGmv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl5x6l%2FdJMcabikiKu%2F4h1zcApYyVb1rQJcpDGmv1%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;329&quot; height=&quot;425&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;1328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-end=&quot;1028&quot; data-start=&quot;906&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1028&quot; data-start=&quot;908&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html?utm_source=chatgpt.com&quot; data-end=&quot;1026&quot; data-start=&quot;908&quot;&gt;MySQL Storage Requirements&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure id=&quot;og_1763947672386&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;MySQL :: MySQL 8.0 Reference Manual :: 13.7 Data Type Storage Requirements&quot; data-og-description=&quot;13.7&amp;nbsp;Data Type Storage Requirements The storage requirements for table data on disk depend on several factors. Different storage engines represent data types and store raw data differently. Table data might be compressed, either for a column or an entire &quot; data-og-host=&quot;dev.mysql.com&quot; data-og-source-url=&quot;https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html?utm_source=chatgpt.com&quot; data-og-url=&quot;https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html?utm_source=chatgpt.com&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;MySQL :: MySQL 8.0 Reference Manual :: 13.7 Data Type Storage Requirements&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;13.7&amp;nbsp;Data Type Storage Requirements The storage requirements for table data on disk depend on several factors. Different storage engines represent data types and store raw data differently. Table data might be compressed, either for a column or an entire&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev.mysql.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1091&quot; data-start=&quot;1029&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1091&quot; data-start=&quot;1029&quot;&gt;CPU와 같은 하드웨어는 1bit 단위로 IO를 처리할 수 없기 때문에 BIT가 1bit만 차지하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;1114&quot; data-start=&quot;1098&quot; data-ke-size=&quot;size26&quot;&gt;JPA에서 Boolean&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czk9dD/dJMcafE1uIp/RqzhDggE0aSQGjgK8H9rs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czk9dD/dJMcafE1uIp/RqzhDggE0aSQGjgK8H9rs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czk9dD/dJMcafE1uIp/RqzhDggE0aSQGjgK8H9rs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fczk9dD%2FdJMcafE1uIp%2FRqzhDggE0aSQGjgK8H9rs0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;270&quot; height=&quot;217&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3ygBj/dJMcagjC15r/PKNIIDXC6SgSuR7jRKJHf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3ygBj/dJMcagjC15r/PKNIIDXC6SgSuR7jRKJHf0/img.png&quot; data-alt=&quot;https://docs.hibernate.org/orm/7.1/userguide/html_single/?utm_source=chatgpt.com#basic-boolean&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3ygBj/dJMcagjC15r/PKNIIDXC6SgSuR7jRKJHf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3ygBj%2FdJMcagjC15r%2FPKNIIDXC6SgSuR7jRKJHf0%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;444&quot; height=&quot;177&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.hibernate.org/orm/7.1/userguide/html_single/?utm_source=chatgpt.com#basic-boolean&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1245&quot; data-start=&quot;1116&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1184&quot; data-start=&quot;1116&quot;&gt;JPA/Hibernate에서 boolean 타입을 매핑하면 MySQL에서는 기본적으로 BIT로 생성될 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;1245&quot; data-start=&quot;1185&quot;&gt;즉, TINYINT(1)으로 매핑하도록 DDL을 직접 작성하거나 확인하는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1763947843972&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE user (
    id BIGINT NOT NULL,
    is_deleted TINYINT(1) NOT NULL,
    PRIMARY KEY (id)
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;1386&quot; data-start=&quot;1368&quot; data-ke-size=&quot;size26&quot;&gt;Boolean vs Enum&lt;/h2&gt;
&lt;p data-end=&quot;1510&quot; data-start=&quot;1388&quot; data-ke-size=&quot;size16&quot;&gt;TINYINT(1)은 단순히 0과 1을 허용하는 것을 넘어, 실수로 다른 값이 들어갈 수도 있다.&lt;br /&gt;따라서 상태가 명확한 경우 Boolean을 쓰되, &lt;b&gt;상태가 확장될 가능성이 있는 경우&lt;/b&gt;에는 Enum을 사용하는 것이 좋다.&lt;/p&gt;
&lt;h3 data-end=&quot;1526&quot; data-start=&quot;1512&quot; data-ke-size=&quot;size23&quot;&gt;Enum 사용 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1609&quot; data-start=&quot;1528&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1558&quot; data-start=&quot;1528&quot;&gt;&quot;Y&quot;, &quot;N&quot; 등 명시적인 상태 표현 가능&lt;/li&gt;
&lt;li data-end=&quot;1589&quot; data-start=&quot;1559&quot;&gt;상태가 추가되더라도 SRP 원칙을 지키며 확장 가능&lt;/li&gt;
&lt;li data-end=&quot;1609&quot; data-start=&quot;1590&quot;&gt;코드 가독성 및 유지보수성 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-end=&quot;1895&quot; data-start=&quot;1610&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1895&quot; data-start=&quot;1612&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/4337942/using-enum-vs-boolean&quot; data-end=&quot;1713&quot; data-start=&quot;1612&quot;&gt;Stack Overflow - Boolean vs Enum&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://softwareengineering.stackexchange.com/questions/147977/is-it-wrong-to-use-a-boolean-parameter-to-determine-behavior&quot; data-end=&quot;1895&quot; data-start=&quot;1718&quot;&gt;Software Engineering SE - Boolean parameter design&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;1907&quot; data-start=&quot;1902&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2067&quot; data-start=&quot;1909&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2010&quot; data-start=&quot;1909&quot;&gt;단순 true/false만 존재하고 확장 가능성이 없다면 &amp;rarr; &lt;b&gt;TINYINT(1) 사용&lt;/b&gt;, 필요 시 DB에 CHECK (is_deleted IN (0,1)) 제약 추가.&lt;/li&gt;
&lt;li data-end=&quot;2067&quot; data-start=&quot;2011&quot;&gt;상태가 여러 가지로 확장될 가능성이 있거나 의미를 명확히 하고 싶다면 &amp;rarr; &lt;b&gt;Enum 사용&lt;/b&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-end=&quot;2155&quot; data-start=&quot;2069&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;2155&quot; data-start=&quot;2071&quot; data-ke-size=&quot;size16&quot;&gt;Boolean과 Enum은 상황에 맞게 선택하고, JPA/Hibernate를 사용할 경우 DB 매핑이 원하는 타입으로 되었는지 확인하는 것이 중요하다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>DB/MySQL</category>
      <category>bit</category>
      <category>bool</category>
      <category>boolean</category>
      <category>db</category>
      <category>enum</category>
      <category>mysql</category>
      <author>27200</author>
      <guid isPermaLink="true">https://to-travel-coding.tistory.com/468</guid>
      <comments>https://to-travel-coding.tistory.com/468#entry468comment</comments>
      <pubDate>Mon, 24 Nov 2025 10:38:24 +0900</pubDate>
    </item>
  </channel>
</rss>