<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>La programmazione &amp;egrave; bella</title>
    <link>https://b-programmer.tistory.com/</link>
    <description>다양하게 해석하며 그것을 나의 것으로 만들기 위해 최선을 다합니다.</description>
    <language>ko</language>
    <pubDate>Fri, 10 Jul 2026 20:47:35 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>klom</managingEditor>
    <image>
      <title>La programmazione &amp;egrave; bella</title>
      <url>https://tistory1.daumcdn.net/tistory/3781572/attach/908468cb7b54469fac4096ee1f266f51</url>
      <link>https://b-programmer.tistory.com</link>
    </image>
    <item>
      <title>레디스의 자료구조들은 굳이 알아야 할까?</title>
      <link>https://b-programmer.tistory.com/588</link>
      <description>&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6Ar1W/dJMcahkM6gX/XLU65TPcM7GEvHfk3GNa21/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6Ar1W/dJMcahkM6gX/XLU65TPcM7GEvHfk3GNa21/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6Ar1W/dJMcahkM6gX/XLU65TPcM7GEvHfk3GNa21/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6Ar1W%2FdJMcahkM6gX%2FXLU65TPcM7GEvHfk3GNa21%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;332&quot; height=&quot;332&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스에는 다양한 명령어와 자료구조가 존재합니다. 하지만 막상 어떤 자료구조들이 있는지 하나씩 찾아보며 학습하는 경우는 많지 않습니다. 제가 그 이유를 생각해 보니 크게 두 가지였습니다. 첫 번째는, 프로그래밍을 학습하면서 리스트, 집합, 맵과 같은 기본적인 자료구조를 이미 익혔다고 생각했습니다. 그래서 자료구조 자체는 이미 알고 있다고 생각하며, 굳이 다시 학습할 필요성을 느끼지 못했습니다. 두 번째는, 세상에는 수많은 자료구조가 존재하기 때문에 모든 것을 미리 학습하는 것은 비효율적이라고 생각했습니다. 필요할 때마다 찾아보고 사용하는 것이 더 효율적이라고 판단하게 된다고 생각하였습니다. 그럼에도 제가 레디스의 자료구조를 학습하기로 마음먹은 이유가 있습니다. 레디스의 자료구조는 단순히 데이터를 저장하는 컨테이너가 아니라, 각 자료구조에 대해 원자적인 연산(Atomic Operation) 을 제공합니다. 그렇다면 과연 모든 자료구조가 어떤 원자적 연산을 제공하며, 이를 어떻게 활용할 수 있을지 궁금해졌습니다. 물론 레디스의 모든 자료구조를 학습할 생각은 없습니다. 하지만 필요한 자료구조를 판단할 수 있을 정도의 이해는 갖추고 싶었습니다. 과연 '필요할 때만 찾아보면 된다'는 과거의 생각이 정말 맞았을까요? 이번 글에서는 그 생각을 직접 깨보려고 합니다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;대표적인 자료구조 5종 부터&amp;nbsp; 파악해봅시다.&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;레디스를 처음 접하면 생각보다 자료구조의 종류가 많아 보입니다. 하지만 실제 서비스에서 가장 많이 사용되는 자료구조는 다음 다섯 가지입니다. String, Hash, List, Set, Sorted Set이 존재합니다.&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjhbKm/dJMcaazaw5w/PU0hdu8x3RjilFmVO1M9L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjhbKm/dJMcaazaw5w/PU0hdu8x3RjilFmVO1M9L1/img.png&quot; data-alt=&quot;stream은 Redis5.0에 추가되었다고 합니다. 기본은 Stream을 제외한 5종이 맞습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjhbKm/dJMcaazaw5w/PU0hdu8x3RjilFmVO1M9L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjhbKm%2FdJMcaazaw5w%2FPU0hdu8x3RjilFmVO1M9L1%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;402&quot; height=&quot;384&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;stream은 Redis5.0에 추가되었다고 합니다. 기본은 Stream을 제외한 5종이 맞습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다섯 가지를 이해하면 대부분의 Redis 활용 사례를 이해할 수 있습니다. 이제 각 자료구조가 어떤 특징을 가지며, 어떤 원자적 연산을 제공하는지 하나씩 알아보겠습니다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;String&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이름 때문에 문자열만 저장하는 자료구조라고 생각하기 쉽습니다. 하지만 Redis의 String은 문자의 연결이 아니라 바이트(Byte)의 연속된 데이터를 의미합니다. 따라서 텍스트뿐만 아니라 숫자, JSON, 직렬화된 객체, 이미지와 같은 바이너리 데이터까지 저장할 수 있습니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;그렇다면, 어째서 Redis는 바이트 단위로 저장할까요?&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;가장&amp;nbsp;큰&amp;nbsp;이유는&amp;nbsp;범용성입니다.&amp;nbsp;문자&amp;nbsp;단위로&amp;nbsp;저장한다면&amp;nbsp;인코딩(UTF-8,&amp;nbsp;EUC-KR&amp;nbsp;등)을&amp;nbsp;신경&amp;nbsp;써야&amp;nbsp;하고,&amp;nbsp;이미지나&amp;nbsp;직렬화된&amp;nbsp;객체&amp;nbsp;같은&amp;nbsp;바이너리&amp;nbsp;데이터는&amp;nbsp;저장하기&amp;nbsp;어려워집니다.&amp;nbsp;바이트&amp;nbsp;단위로&amp;nbsp;저장하면&amp;nbsp;이런&amp;nbsp;구분&amp;nbsp;없이&amp;nbsp;모든&amp;nbsp;데이터를&amp;nbsp;그대로&amp;nbsp;담을&amp;nbsp;수&amp;nbsp;있습니다.&lt;br&gt;이를 실제로 저장해봅시다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;194&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpXGMh/dJMcadCvoli/PHq7kWJlulMbttjdnmS8yK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpXGMh/dJMcadCvoli/PHq7kWJlulMbttjdnmS8yK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpXGMh/dJMcadCvoli/PHq7kWJlulMbttjdnmS8yK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpXGMh%2FdJMcadCvoli%2FPHq7kWJlulMbttjdnmS8yK%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;194&quot; height=&quot;64&quot; data-origin-width=&quot;194&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이모지를 저장하고 Get을 시켜보겠습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdgcbF/dJMb99Uv1gX/d0UalejkEMErgkK2KtBdc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdgcbF/dJMb99Uv1gX/d0UalejkEMErgkK2KtBdc1/img.png&quot; data-alt=&quot;테이블&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdgcbF/dJMb99Uv1gX/d0UalejkEMErgkK2KtBdc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdgcbF%2FdJMb99Uv1gX%2Fd0UalejkEMErgkK2KtBdc1%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;512&quot; height=&quot;122&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;122&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;984&quot; data-origin-height=&quot;72&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kBDGH/dJMcabdFD4F/rKBsD94947LStDyVFvWW40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kBDGH/dJMcabdFD4F/rKBsD94947LStDyVFvWW40/img.png&quot; data-alt=&quot;트리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kBDGH/dJMcabdFD4F/rKBsD94947LStDyVFvWW40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkBDGH%2FdJMcabdFD4F%2FrKBsD94947LStDyVFvWW40%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;984&quot; height=&quot;72&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;72&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;트리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이트단위로 저장이 된것을 확인 할 수 있었습니다.&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;그렇다면, String으로 저장된 데이터는 어떤식으로 이용할 수 있을까요?&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;String은&amp;nbsp;데이터를&amp;nbsp;바이트&amp;nbsp;단위로&amp;nbsp;저장합니다.&amp;nbsp;Redis는&amp;nbsp;싱글&amp;nbsp;스레드로&amp;nbsp;명령을&amp;nbsp;처리하기&amp;nbsp;때문에,&amp;nbsp;String의&amp;nbsp;모든&amp;nbsp;단일&amp;nbsp;명령은&amp;nbsp;원자적으로&amp;nbsp;실행됩니다.&amp;nbsp;즉,&amp;nbsp;여러&amp;nbsp;클라이언트가&amp;nbsp;동시에&amp;nbsp;같은&amp;nbsp;값을&amp;nbsp;조작해도&amp;nbsp;각&amp;nbsp;명령은&amp;nbsp;끊김&amp;nbsp;없이&amp;nbsp;실행됩니다.&amp;nbsp;이러한&amp;nbsp;원자성&amp;nbsp;덕분에&amp;nbsp;String은&amp;nbsp;캐시,&amp;nbsp;분산&amp;nbsp;락,&amp;nbsp;카운터&amp;nbsp;등&amp;nbsp;값&amp;nbsp;하나를&amp;nbsp;안전하게&amp;nbsp;다뤄야&amp;nbsp;하는&amp;nbsp;상황에서&amp;nbsp;널리&amp;nbsp;활용됩니다.&lt;br&gt;하지만 이 원자성은 '단일 값' 단위입니다. 만약 논리적으로 함께 다뤄져야 할 값들이 여럿이라면, 예를 들어 사용자의 이름과 나이가 언제나 함께 갱신되어야 한다면&amp;nbsp; String을 두 개 사용해서는 원자성을 확보할 수 없습니다. 이런 경우엔 필드 단위로 여러 값을 함께 관리할 수 있는 Hash 같은 다른 자료구조가 필요합니다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Hash&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Hash는 String과 달리 하나의 키 안에서 필드 단위로 접근/수정이 가능합니다. 앞서&amp;nbsp;Redis의&amp;nbsp;명령이&amp;nbsp;원자적으로&amp;nbsp;실행된다는&amp;nbsp;점을&amp;nbsp;살펴봤습니다.&amp;nbsp;그렇다면&amp;nbsp;Hash&amp;nbsp;역시&amp;nbsp;원자성을&amp;nbsp;가진다는&amp;nbsp;뜻이&amp;nbsp;됩니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;그렇다면 Hash의 원자성은 String과 무엇이 다를까요?&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;100명의 사용자가 동시에 사용자 정보를 수정하는 상황을 가정해 보겠습니다.&lt;br&gt;먼저 String으로 사용자 정보를 저장했다고 가정해 보겠습니다. 이름과 나이를 각각 다른 키에 저장하고 있다면, 이름을 변경하는 SET 명령과 나이를 변경하는 SET 명령은 서로 별개의 명령으로 실행됩니다.&lt;br&gt;예를&amp;nbsp;들어&amp;nbsp;A&amp;nbsp;클라이언트가&amp;nbsp;이름을&amp;nbsp;먼저&amp;nbsp;변경했다고&amp;nbsp;하겠습니다.&amp;nbsp;아직&amp;nbsp;나이를&amp;nbsp;변경하기&amp;nbsp;전에&amp;nbsp;B&amp;nbsp;클라이언트가&amp;nbsp;데이터를&amp;nbsp;조회한다면&amp;nbsp;새로운 이름과 기존 나이를 함께 조회할 수 있습니다. 즉, 하나의 사용자 정보가 중간 상태로 노출되어 일관성이 깨질 수 있습니다.&lt;br&gt;반면 Hash는 하나의 HSET 명령으로 여러 필드를 동시에 수정할 수 있습니다. 예를 들어 이름과 나이를 하나의 HSET 명령으로 변경한다면, 해당 명령은 원자적으로 실행됩니다. 따라서 다른 클라이언트는 수정이 모두 완료된 상태 또는 수정되기 전 상태만 조회할 수 있으며, 이름만 변경되고 나이는 변경되지 않은 중간 상태를 볼 수 없습니다.&lt;br&gt;즉, Hash가 String보다 더 원자적인 자료구조인 것이 아닙니다. 두 자료구조 모두 Redis 명령은 원자적으로 실행됩니다. 차이점은 String은 여러 값을 수정하려면 여러 개의 명령이 필요하지만, Hash는 여러 필드를 하나의 명령으로 수정할 수 있다는 점입니다. 이 차이로 인해 여러 필드를 함께 관리해야 하는 경우에는 Hash가 데이터의 일관성을 유지하기에 더 적합합니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;그렇다면, Hash는 언제 사용할까요?&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;String과&amp;nbsp;Hash가&amp;nbsp;크게&amp;nbsp;다르지&amp;nbsp;않으니&amp;nbsp;사용처도&amp;nbsp;비슷할&amp;nbsp;것이라&amp;nbsp;예상할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;이는&amp;nbsp;반은&amp;nbsp;맞고&amp;nbsp;반은&amp;nbsp;틀린&amp;nbsp;답입니다.&lt;br&gt;사용처의 카테고리 자체는 비슷합니다. &lt;br&gt;캐싱, 세션, 카운터 등 String이 잘 쓰이는 곳에서 Hash도 자주 쓰입니다. 다만 어느 쪽이 더 자연스러운지는 상황에 따라 다릅니다. &amp;nbsp;분산 락, 바이너리 통 저장, 단순 캐시된 문자열 같은 경우는 String이 더 자연스럽습니다. 반면 하나의 객체를 이루는 여러 필드를 관리하거나, 부분 갱신이 잦은 상태를 다뤄야 한다면 Hash가 더 자연스럽습니다.&lt;br&gt;&amp;nbsp;즉, 어떤 자료구조를 쓰느냐는 개발자의 재량이지만, '자연스러움'을 기준으로 판단하는 것이 좋습니다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;List&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터&amp;nbsp;단일&amp;nbsp;객체가&amp;nbsp;아닌,&amp;nbsp;여러&amp;nbsp;객체를&amp;nbsp;다루는&amp;nbsp;자료구조들을&amp;nbsp;알아봅시다.&amp;nbsp;그&amp;nbsp;중&amp;nbsp;가장&amp;nbsp;먼저&amp;nbsp;학습할&amp;nbsp;자료구조는&amp;nbsp;List입니다.&lt;br&gt;프로그래밍&amp;nbsp;언어에서&amp;nbsp;List는&amp;nbsp;여러&amp;nbsp;값을&amp;nbsp;순서대로&amp;nbsp;저장할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;자료구조입니다.&amp;nbsp;&lt;br&gt;예를&amp;nbsp;들어&amp;nbsp;자바로&amp;nbsp;작성하면&amp;nbsp;다음과&amp;nbsp;같습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; strs = new ArratList&amp;lt;&amp;gt;();

strs.add(&quot;1&quot;);
strs.add(&quot;2&quot;);
strs.add(&quot;3&quot;);&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;List는&amp;nbsp;입력한&amp;nbsp;순서를&amp;nbsp;유지하고,&amp;nbsp;중복된&amp;nbsp;값도&amp;nbsp;허용한다는&amp;nbsp;특징이&amp;nbsp;있습니다.&amp;nbsp;Redis에서는&amp;nbsp;이러한&amp;nbsp;List를&amp;nbsp;어떻게&amp;nbsp;다루는지&amp;nbsp;확인해봅시다.&lt;br&gt;단일 값으로는 String과 Hash가 있습니다. (굳이 따진다면, String은 값이고 Hash는 객체지만 딱히 분류하지는 않겠습니다.)&lt;br&gt;그렇다면, Redis List에는 어떠한 값이 들어갈 수 있을까요? 그리고 어떻게 사용할까요?&lt;br&gt;결론부터 말하면, Redis List의 원소로는 String(바이트 시퀀스)만 넣을 수 있습니다. Hash처럼 구조화된 자료구조를 List 원소로 담는 것은 불가능합니다. 이는 Redis의 설계 철학에서 비롯됩니다. Redis는 '단순함'을 최우선으로 두어, 자료구조의 중첩을 허용하지 않습니다. 모든 자료구조는 최상위 키에만 존재하며, 그 안의 원소 값은 언제나 바이트 시퀀스입니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;그렇다면, List 자료구조는 어떻게 원자성을 지킬 수 있을까?&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;List의&amp;nbsp;원자성은&amp;nbsp;3가지&amp;nbsp;레벨로&amp;nbsp;나눠&amp;nbsp;생각할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br&gt;기본적으로 Redis는 싱글 스레드로 명령을 처리하기 때문에 List의 모든 단일&amp;nbsp;명령이&amp;nbsp;원자적으로&amp;nbsp;실행됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;RPUSH tasks &quot;job&quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ 원자적 추가
LPOP tasks&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; → 원자적 꺼내기
LRANGE tasks 0 -1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ 원자적 스냅샷 조회&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 여러 클라이언트가 동시에 조작해도 각 명령은 끊김 없이 실행됩니다. 원자성의 원천은 자료구조가 아닌 Redis의 실행 모델에 있기 때문입니다. 그 다음으로, Hash가 여러 필드를 한 명령으로 다뤘듯 List도 여러 원소를 한&amp;nbsp;명령으로&amp;nbsp;처리할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;RPUSH tasks &quot;job1&quot; &quot;job2&quot; &quot;job3&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;세 원소가 원자적으로 한번에 추가되고, 중간에 다른 클라이언트가 낄 수 없습니다. 이 덕분에 큐잉 작업을 안전하게 처리할 수 있습니다.&lt;br&gt;마지막으로,&amp;nbsp;List에는&amp;nbsp;리스트&amp;nbsp;간&amp;nbsp;원자적&amp;nbsp;이동이라는&amp;nbsp;특수&amp;nbsp;연산이&amp;nbsp;있습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;LMOVE (또는 옛 RPOPLPUSH)
LMOVE source dest LEFT RIGHT&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;한 리스트에서 꺼내 다른 리스트로 옮기는 작업이 한 명령으로 이뤄지기 때문에 원자적입니다. 이것이 중요한 이유는, 만약 두 명령으로 나눠 처리한다면 다음과&amp;nbsp;같은&amp;nbsp;유실&amp;nbsp;위험이&amp;nbsp;있기&amp;nbsp;때문입니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;LPOP source&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; → &quot;job&quot; 꺼냄
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;↓ 여기서 서버 죽으면?
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ job 유실!
RPUSH dest &quot;job&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;LMOVE는 이 사이의 유실 가능성을 원천 차단합니다. 또한, BLPOP 같은 블로킹 팝 명령도 있습니다. 값이 없으면 원자적으로 대기하고, 값이 생기면 원자적으로 꺼내옵니다. 폴링 없이 안전한 큐 소비 패턴에 활용됩니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;그렇다면, List는 어떻게 사용 할까요?&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;List는 앞서 살펴봤듯이 순서가 존재하며, 중복을 허용하는 자료구조입니다. 그렇기 때문에 입력한 순서를 유지해야 하는 상황에서 주로 사용됩니다. 예를&amp;nbsp;들어&amp;nbsp;메시지&amp;nbsp;큐,&amp;nbsp;최근&amp;nbsp;목록,&amp;nbsp;히스토리,&amp;nbsp;스택&amp;nbsp;등에서&amp;nbsp;활용할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Set&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 List와 비교하면, Set은 크게 달라 보이지 않을 수 있습니다. 차이점은 중복을 허용하지 않고, 순서 또한 유지하지 않는다는 점입니다. 프로그래밍 언어의 Set 자료구조를 떠올려보면, 중복을 자동으로 제거해주고 존재&amp;nbsp;확인이&amp;nbsp;빠르다는&amp;nbsp;특징이&amp;nbsp;있습니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;Redis에서 Set은 어떤 특별함이 있을까?&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;사실 프로그래밍 Set과 굉장히 유사합니다. 하지만 Redis는 모든 값들을 원자단위로 관리합니다. 전통적으로 Set은 집합 연산에 능합니다. 이를 Redis에서는 원자단위로 할 수 있다는 점입니다. 예를 들어, 교집합, 합집합, 차집합이 있습니다.&lt;br&gt;만약 프로그래밍상에서 이들을 구하려면 여러 연산이 뒤섞여서 찾아야 합니다. &lt;br&gt;&lt;br&gt;Set의 진짜 특별함은 집합 연산에 있습니다. 교집합, 합집합, 차집합&amp;nbsp;같은&amp;nbsp;연산을&amp;nbsp;원자적인&amp;nbsp;단일&amp;nbsp;명령으로&amp;nbsp;처리할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br&gt;예를 들어 Set A에는 {1, 2, 3}이, Set B에는 {3, 4, 5}가 있다고 해봅시다.&lt;br&gt;&lt;br&gt;-&amp;nbsp;합집합&amp;nbsp;A&amp;nbsp;∪&amp;nbsp;B&amp;nbsp;=&amp;nbsp;{1,&amp;nbsp;2,&amp;nbsp;3,&amp;nbsp;4,&amp;nbsp;5}&lt;br&gt;-&amp;nbsp;교집합&amp;nbsp;A&amp;nbsp;∩&amp;nbsp;B&amp;nbsp;=&amp;nbsp;{3}&lt;br&gt;-&amp;nbsp;차집합&amp;nbsp;A&amp;nbsp;-&amp;nbsp;B&amp;nbsp;=&amp;nbsp;{1,&amp;nbsp;2}&lt;br&gt;&lt;br&gt;일반적인 프로그래밍 언어에서 이런 연산을 하려면 두 집합을 순회하며 조건을 검사하는 여러 단계가 필요합니다. Redis에서는 이를 명령 하나로 원자적으로 처리합니다. 게다가 서로 다른 클라이언트가 각기 관리하는 집합에 대해서도 동일하게 동작하기 때문에,&amp;nbsp;분산&amp;nbsp;환경에서의&amp;nbsp;집합&amp;nbsp;관계&amp;nbsp;계산도&amp;nbsp;자연스럽게&amp;nbsp;가능해집니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;그렇다면 어떻게 활용되어질까?&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;복습해보면, List는 중복은 상관없고 순서가 중요한 경우에 주로 사용된다고 했습니다. 반면 Set의 가장 큰 특징은 중복을 허용하지 않는다는 점입니다. 대표적인 활용 예로는 태그 관리, 팔로우 관계, 방문자 분석, 좋아요&amp;nbsp;이력,&amp;nbsp;권한&amp;nbsp;관리&amp;nbsp;등이&amp;nbsp;있습니다.&amp;nbsp;중복&amp;nbsp;없이&amp;nbsp;값을&amp;nbsp;담아야&lt;br&gt;하거나, 특정 값의 존재 여부를 빠르게 확인해야 하거나, 두 집합의&amp;nbsp;공통&amp;nbsp;원소나&amp;nbsp;차이를&amp;nbsp;계산해야&amp;nbsp;하는&amp;nbsp;경우&amp;nbsp;Set이&amp;nbsp;잘&amp;nbsp;어울립니다.&lt;br&gt;&lt;br&gt;특히 팔로우 관계에서 '공통 친구', 방문자 분석에서 '충성 유저 vs&amp;nbsp;신규&amp;nbsp;유저&amp;nbsp;vs&amp;nbsp;이탈&amp;nbsp;유저'&amp;nbsp;같은&amp;nbsp;계산이&amp;nbsp;Set의&amp;nbsp;집합&amp;nbsp;연산&amp;nbsp;하나로&lt;br&gt;해결됩니다.&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sorted Set&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Sorted Set은 특별합니다. 단순한 Set이 아니라 정렬까지 지원하기 때문입니다.&amp;nbsp;Set과&amp;nbsp;마찬가지로&amp;nbsp;중복은&amp;nbsp;제거되지만,&amp;nbsp;각&amp;nbsp;원소에 ’점수(score)‘를&amp;nbsp;부여해&amp;nbsp;그&amp;nbsp;점수를&amp;nbsp;기준으로&amp;nbsp;자동&amp;nbsp;정렬됩니다.&lt;br&gt;&lt;br&gt;그런데 여기서 한 가지 의문이 생깁니다. List나 Set은 원소를 추가하기만 합니다. 하지만 Sorted Set은 원소 추가와 함께 정렬까지 수행합니다. 한 명령 안에서 두 가지 일이 벌어지는 셈인데, 이것이 원자적으로 가능할까요?&amp;nbsp;그리고&amp;nbsp;성능은&amp;nbsp;어떻게&amp;nbsp;유지될까요?&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;Sorted Set이 원자적으로 정렬과 원소 추가를 할 수 는 방법?&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;원자성의 원천은 다른 자료구조와 같습니다. Redis는 싱글 스레드로 명령을 하나씩 처리하기 때문에, 명령 안에서 아무리 여러 단계를 거치더라도 그 명령이 실행되는 동안 다른 클라이언트가 끼어들 수 없습니다. 그렇다면 성능은 어떻게 유지될까요? 원소를 넣을 때마다 전체를 정렬한다면 원소가 많아질수록 급격히 느려질 것입니다. Redis는 Skip&amp;nbsp;List라는&amp;nbsp;자료구조를&amp;nbsp;활용해&amp;nbsp;이&amp;nbsp;문제를&amp;nbsp;해결합니다.&amp;nbsp;Skip&amp;nbsp;List는 '매번 정렬'하는 것이 아니라 '항상 정렬된 상태를 유지'하는 자료구조로,&amp;nbsp;원소가&amp;nbsp;100만&amp;nbsp;개여도&amp;nbsp;삽입&amp;nbsp;위치를&amp;nbsp;로그&amp;nbsp;시간에&amp;nbsp;찾아냅니다.&lt;br&gt;&lt;br&gt;즉, Sorted Set은 원자적인 동시에 빠릅니다. Redis의 실행 모델이 원자성을&amp;nbsp;보장하고,&amp;nbsp;Skip&amp;nbsp;List의&amp;nbsp;특성이&amp;nbsp;성능을&amp;nbsp;보장합니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;그렇다면, Sorted Set은 어떻게 활용할까요?&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Sorted Set은 순위, 우선순위, 시간 순서 같은 '점수로 표현할 수 있는 정렬 기준'이 필요할 때 자연스럽습니다. 대표적인 활용 예로는 실시간 랭킹(게임 점수, 인기 게시글), 우선순위 큐(긴급 작업 먼저), 시계열 데이터(활동 이력, 로그), 지연 큐(예약 발송),&amp;nbsp;자동완성(빈도&amp;nbsp;기반&amp;nbsp;추천)&amp;nbsp;등이&amp;nbsp;있습니다.&lt;br&gt;&lt;br&gt;특히 실시간 랭킹에서 점수 증가와 순위 재정렬이 명령 하나로 원자적으로 처리된다는 점이 강력합니다. 일반적인 데이터베이스에서는 트랜잭션과 잠금을&amp;nbsp;신경&amp;nbsp;써야&amp;nbsp;하는&amp;nbsp;작업이&amp;nbsp;Sorted&amp;nbsp;Set에서는&amp;nbsp;자연스럽게&amp;nbsp;해결됩니다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 Stream이라는 자료구조가 5.0에서 추가되었습니다. 로그형 자료구조로 이벤트 스트리밍이나 메시지 큐에 활용되지만, 개념이 다른 차원이라 별도 학습이 필요합니다. 이 글에서는 기본 5개 자료구조에 집중합니다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;모든 자료구조의 기본적인 명령어를 표로 알아보자.&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가 (ADD/SET/PUSH)&lt;/h3&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style1&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;패턴&lt;/td&gt;&lt;td style=&quot;text-align: center;&quot;&gt;String&lt;/td&gt;&lt;td style=&quot;text-align: center;&quot;&gt;Hash&lt;/td&gt;&lt;td style=&quot;text-align: center;&quot;&gt;List&lt;/td&gt;&lt;td style=&quot;text-align: center;&quot;&gt;Set&lt;/td&gt;&lt;td style=&quot;text-align: center;&quot;&gt;Sorted Set&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;기본 추가&lt;/td&gt;&lt;td&gt;SET&lt;/td&gt;&lt;td&gt;HSET&lt;/td&gt;&lt;td&gt;LPUSH / RPUSH&lt;/td&gt;&lt;td&gt;SADD&lt;/td&gt;&lt;td&gt;ZADD&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;조건부 추가&lt;/td&gt;&lt;td&gt;SET ... NX&lt;/td&gt;&lt;td&gt;HSETNX&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;ZADD ... NX&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;조회 (GET)&lt;/h3&gt;&lt;div data-sourcepos=&quot;12:1-16:75;260-538&quot;&gt; 
 &lt;table style=&quot;border-collapse: collapse; width: 100%; height: 74px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt; 
  &lt;tbody&gt; 
   &lt;tr style=&quot;height: 17px;&quot;&gt; 
    &lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;패턴&lt;/td&gt; 
    &lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;String&lt;/td&gt; 
    &lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;Hash&lt;/td&gt; 
    &lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;List&lt;/td&gt; 
    &lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;Set&lt;/td&gt; 
    &lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;Sorted Set&lt;span&gt;&lt;/span&gt;&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style=&quot;height: 19px;&quot;&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;단건 조회&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;GET&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;HGET&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;LINDEX&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;SISMEMBER&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;ZSCORE&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style=&quot;height: 19px;&quot;&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;다건 조회&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;MGET&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;HMGET&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;LRANGE&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;SMISMEMBER&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;ZRANGE&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style=&quot;height: 19px;&quot;&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;전체 조회&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;GET&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;HGETALL&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;LRANGE 0 -1&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;SMEMBERS&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;ZRANGE 0 -1&lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;삭제 (DEL / REM)&lt;/h3&gt;&lt;div data-sourcepos=&quot;20:1-23:78;563-775&quot;&gt; 
 &lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style7&quot;&gt; 
  &lt;tbody&gt; 
   &lt;tr&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;패턴&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;String&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;Hash&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;List&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;Set&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;Sorted Set&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr&gt; 
    &lt;td&gt;값/필드 삭제&lt;/td&gt; 
    &lt;td&gt;DEL (키)&lt;/td&gt; 
    &lt;td&gt;HDEL&lt;/td&gt; 
    &lt;td&gt;LREM&lt;/td&gt; 
    &lt;td&gt;SREM&lt;/td&gt; 
    &lt;td&gt;ZREM&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr&gt; 
    &lt;td&gt;꺼내면서 삭제&lt;/td&gt; 
    &lt;td&gt;GETDEL&lt;/td&gt; 
    &lt;td&gt;-&lt;/td&gt; 
    &lt;td&gt;LPOP / RPOP&lt;/td&gt; 
    &lt;td&gt;SPOP&lt;/td&gt; 
    &lt;td&gt;ZPOPMIN / ZPOPMAX&lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;개수 (LEN / CARD)&lt;/h3&gt;&lt;div data-sourcepos=&quot;27:1-29:62;801-937&quot;&gt; 
 &lt;table style=&quot;border-collapse: collapse; width: 100%; height: 38px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style6&quot;&gt; 
  &lt;tbody&gt; 
   &lt;tr style=&quot;height: 19px;&quot;&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;패턴&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;String&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Hash&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;List&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Set&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Sorted Set&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style=&quot;height: 19px;&quot;&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;개수&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;STRLEN (바이트)&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;HLEN&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;LLEN&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;SCARD&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;ZCARD&lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;존재 확인 (EXISTS)&lt;/h3&gt;&lt;div data-sourcepos=&quot;33:1-35:75;962-1111&quot;&gt; 
 &lt;table style=&quot;border-collapse: collapse; width: 100%; height: 38px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt; 
  &lt;tbody&gt; 
   &lt;tr style=&quot;height: 19px;&quot;&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;패턴&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;String&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Hash&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;List&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Set&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Sorted Set&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style=&quot;height: 19px;&quot;&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;존재 확인&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;EXISTS (키)&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;HEXISTS&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;-&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;SISMEMBER&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;ZSCORE (nil 여부)&lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;증가 (INCRBY)&lt;/h3&gt;&lt;div data-sourcepos=&quot;39:1-41:62;1133-1269&quot;&gt; 
 &lt;table style=&quot;border-collapse: collapse; width: 100%; height: 38px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style7&quot;&gt; 
  &lt;tbody&gt; 
   &lt;tr style=&quot;height: 19px;&quot;&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;패턴&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;String&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Hash&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;List&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Set&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Sorted Set&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style=&quot;height: 19px;&quot;&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;원자 증가&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;INCR / INCRBY&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;HINCRBY&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;-&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;-&lt;/td&gt; 
    &lt;td style=&quot;height: 19px;&quot;&gt;ZINCRBY&lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;반복 순회 (SCAN)&lt;/h3&gt;&lt;div data-sourcepos=&quot;45:1-47:58;1292-1424&quot;&gt; 
 &lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style6&quot;&gt; 
  &lt;tbody&gt; 
   &lt;tr&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;패턴&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;String&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;Hash&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;List&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;Set&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;Sorted Set&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;대용량 순회&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;SCAN (키)&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;HSCAN&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;-&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;SSCAN&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot;&gt;ZSCAN&lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;블로킹 (B 접두사)&lt;/h3&gt;&lt;div data-sourcepos=&quot;51:1-53:58;1446-1544&quot;&gt; 
 &lt;table style=&quot;border-collapse: collapse; width: 100%; height: 38px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style5&quot;&gt; 
  &lt;tbody&gt; 
   &lt;tr style=&quot;height: 19px;&quot;&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;패턴&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;List&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Sorted Set&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style=&quot;height: 19px;&quot;&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;블로킹 꺼내기&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;BLPOP / BRPOP&lt;/td&gt; 
    &lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;BZPOPMIN / BZPOPMAX&lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;예외&lt;/h3&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 114px;&quot; border=&quot;1&quot; data-ke-style=&quot;style15&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;예외 명령&lt;/td&gt;&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;이유&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;MSET / MGET&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;Multiple의 M (String 전용)&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;GETSET / GETDEL / GETEX&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;복합 동작 (String)&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;SINTER / SUNION / SDIFF&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;Set 집합 연산 (교/합/차)&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;LMOVE / RPOPLPUSH&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;List 간 원자 이동&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 19px;&quot;&gt;&lt;td style=&quot;height: 19px;&quot;&gt;ZRANGEBYSCORE / ZRANGEBYLEX&lt;/td&gt;&lt;td style=&quot;height: 19px;&quot;&gt;Sorted Set 범위 종류&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Redis의 자료구조에 대해 살펴봤습니다. &amp;nbsp;학습을 진행하면서 Redis의 모든 자료구조가 원자적으로 동작한다는 것을 알게 되었습니다. 그러다보니 자연스럽게 &quot;어떻게 원자적으로 가능한지&quot;가 궁금해졌습니다. 여러 자료를 찾아보며 '아, 이래서 원자적이구나' 하고 깨닫게 되었습니다.&lt;br&gt;또한 자료구조들이 비슷해 보여도 각자의 사용처가 확실하게 구분된다는 점을 느꼈습니다. 처음에는 &quot;필요할 때만 찾아보면되겠지&quot; 싶었지만, 원리를 이해하고 나니 언제 어떤 자료구조를 선택해야 하는지 감이 잡히더군요.&lt;br&gt;몇 가지 남은 아쉬움도 있습니다. 첫째, Stream이라는 자료구조는 다루지 않았습니다. 지금까지 살펴본 자료구조와 성격이 상이하다고 느꼈기 때문입니다. 기본 자료구조와는 다른 카테고리로 느껴져 이 글에 함께 담기에는 무리가 있었습니다. 이 부분은 나중에 별도의 글로 풀어보려고 합니다. 둘째, 각 자료구조 간 비교가 충분하지 않은 것도 사실입니다. 자바 자료구조와의 대비를 통해 실무 감각을 더 살려볼 수 있을텐데, 이 부분도 다음 기회에 시도해보려고 합니다.&lt;br&gt;&lt;br&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/588</guid>
      <comments>https://b-programmer.tistory.com/588#entry588comment</comments>
      <pubDate>Wed, 8 Jul 2026 08:58:44 +0900</pubDate>
    </item>
    <item>
      <title>레디스가 싱글 스레드임에도 불구하고 빠른이유</title>
      <link>https://b-programmer.tistory.com/587</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일반적으로는 싱글 스레드보다 멀티 스레드가 더 빠르다고 생각합니다. 실제로도 혼자 일하는 것보다 여러 사람이 함께 일하는 편이 더 많은 일을 처리할 수 있습니다. &lt;/span&gt;&lt;span&gt;하지만 우리가 간과하기 쉬운 사실이 하나 있습니다. 바로 &lt;/span&gt;&lt;span&gt;락(Lock)&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;오늘날 Redis, Nginx, Node.js처럼 싱글 스레드 기반의 이벤트 루프를 활용하는 기술들은 이러한 질문에 대한 하나의 답을 보여 주고 있습니다. &lt;/span&gt;&lt;span&gt;하지만 이러한 발상이 처음부터 존재했던 것은 아닙니다. &lt;/span&gt;&lt;span&gt;그 시작은 1999년 Dan Kegel이 제시한 &lt;/span&gt;&lt;span&gt;C10K 문제&lt;/span&gt;&lt;span&gt;에 있습니다. 당시 서버는 1만 개의 동시 연결조차 효율적으로 처리하지 못했고, 기존의 멀티 스레드 멀티 프로세스 모델은 한계를 드러내고 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 글에서는 &lt;/span&gt;&lt;span&gt;C10K 문제가 무엇이었는지&lt;/span&gt;&lt;span&gt;, 왜 기존 방식으로는 해결하기 어려웠는지, 그리고 이 문제가 이후의 서버 아키텍처와 이벤트 기반 프로그래밍에 어떤 영향을 주었는지 살펴보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;C10K 문제란 무엇일까?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;생각보다 문제는 단순합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&quot;서버가 동시에 10,000개의 클라이언트 연결을 처리하려면 어떻게 해야 할까?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 질문이 바로 &lt;/span&gt;&lt;span&gt;C10K(C10,000) 문제&lt;/span&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1999년 Dan Kegel은 당시 서버 아키텍처가 10,000개의 동시 연결을 효율적으로 처리하지 못한다는 점을 지적했습니다. 당시에는 연결마다 하나의 프로세스나 스레드를 생성하는 방식이 일반적이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 동시에 10,000명의 사용자가 서버에 접속한다면, 서버는 최대 10,000개의 스레드를 생성하게 됩니다. 처음에는 각 스레드가 독립적으로 요청을 처리하므로 효율적으로 보일 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 스레드가 많아질수록 문제가 발생합니다. 각 스레드는 메모리를 차지할 뿐만 아니라, 운영체제는 여러 스레드를 번갈아 실행하기 위해 끊임없이 &lt;/span&gt;&lt;span&gt;문맥 교환(Context Switching)&lt;/span&gt;&lt;span&gt; 을 수행해야 합니다. 결국 CPU는 실제 요청을 처리하는 시간보다 스레드를 전환하는 데 더 많은 시간을 사용하게 되고, 서버의 성능은 급격히 저하됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;span&gt;그렇다면 정말 연결마다 스레드를 하나씩 생성해야만 할까요? &lt;/span&gt;&lt;span&gt;하나의 스레드가 여러 연결을 효율적으로 관리할 수는 없을까요?&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;C10K 문제는 바로 이러한 질문에서 시작되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;I/O 멀티 플렉싱(I/O Multiplexing)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;C10K 문제를 해결하기 위한 방법은 여러 가지가 있지만, 그중 가장 대표적인 기술이 &lt;/span&gt;&lt;b&gt;&lt;span&gt;I/O 멀티플렉싱(I/O Multiplexing)&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;멀티플렉싱(Multiplexing)을 직역하면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;'여러 개를 하나로 처리한다'&lt;/span&gt;&lt;/b&gt;&lt;span&gt;는 의미를 가지고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기존의 서버는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;연결(Connection) 하나당 하나의 스레드를 생성&lt;/span&gt;&lt;/b&gt;&lt;span&gt;하여 요청을 처리했습니다. 따라서 동시에 연결되는 클라이언트가 많아질수록 스레드 수도 함께 증가했고, 메모리 사용량과 문맥 교환(Context Switching) 비용도 커질 수밖에 없었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qo670/dJMcaicUd7U/Goudq3t03MeKk1xnf0TkHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qo670/dJMcaicUd7U/Goudq3t03MeKk1xnf0TkHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qo670/dJMcaicUd7U/Goudq3t03MeKk1xnf0TkHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqo670%2FdJMcaicUd7U%2FGoudq3t03MeKk1xnf0TkHk%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;274&quot; height=&quot;224&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반면, &lt;/span&gt;&lt;b&gt;&lt;span&gt;I/O 멀티플렉싱은 하나의 스레드가 여러 연결을 동시에 감시하다가 이벤트가 발생한 연결만 선택적으로 처리하는 방식&lt;/span&gt;&lt;/b&gt;&lt;span&gt;입니다. 즉, 모든 연결에 스레드를 할당하는 것이 아니라, 하나의 스레드가 여러 연결을 효율적으로 관리할 수 있도록 만든 기술입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;덕분에 적은 수의 스레드만으로도 수많은 동시 연결을 처리할 수 있게 되었으며, C10K 문제를 해결하는 핵심 기술로 자리 잡게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Yherz/dJMcabxYQKv/1pB10EicmkiueNXZhW1b0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Yherz/dJMcabxYQKv/1pB10EicmkiueNXZhW1b0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Yherz/dJMcabxYQKv/1pB10EicmkiueNXZhW1b0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYherz%2FdJMcabxYQKv%2F1pB10EicmkiueNXZhW1b0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;621&quot; height=&quot;91&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I/O 멀티플렉싱을 기반으로 select, poll, epoll, kqueue와 같은 다양한 기술이 등장했습니다.&lt;/p&gt;
&lt;p data-end=&quot;436&quot; data-start=&quot;335&quot; data-ke-size=&quot;size16&quot;&gt;이러한 기술의 발전으로 하나의 스레드가 여러 연결을 지속적으로 감시하고, 이벤트가 발생한 연결만 처리하는 &lt;b&gt;이벤트 루프(Event Loop)&lt;/b&gt; 방식이 널리 사용되기 시작했습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;436&quot; data-start=&quot;335&quot; 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;344&quot; data-origin-height=&quot;330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FTvjv/dJMcaicVhYJ/IakYP31azPPvIx87mXNM5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FTvjv/dJMcaicVhYJ/IakYP31azPPvIx87mXNM5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FTvjv/dJMcaicVhYJ/IakYP31azPPvIx87mXNM5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFTvjv%2FdJMcaicVhYJ%2FIakYP31azPPvIx87mXNM5K%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;291&quot; height=&quot;279&quot; data-origin-width=&quot;344&quot; data-origin-height=&quot;330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이벤트 루프(Event Loop)라는 이름을 보면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;이벤트를 반복적으로 처리하는 구조&lt;/span&gt;&lt;/b&gt;&lt;span&gt;라는 의미를 떠올릴 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제로 이벤트 루프는 하나의 스레드에서 계속 실행되며, 이벤트가 발생한 연결을 확인하고 해당 작업을 처리하는 실행 방식입니다. 즉, 모든 연결에 스레드를 생성하는 것이 아니라 하나의 스레드가 지속적으로 이벤트를 감시하고, 이벤트가 발생한 연결만 선택적으로 처리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이러한 방식 덕분에 적은 수의 스레드만으로도 많은 동시 연결을 효율적으로 처리할 수 있습니다.&lt;/span&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;구현&lt;/h3&gt;
&lt;pre id=&quot;code_1783092005332&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EventLoop {

    public static void main(String[] args) throws Exception {

        List&amp;lt;Connection&amp;gt; connections = List.of(
                new Connection(&quot;Client-1&quot;),
                new Connection(&quot;Client-2&quot;),
                new Connection(&quot;Client-3&quot;)
        );

        while (server.isRunning()) {

            // 이벤트가 발생한 연결만 가져온다.
            List&amp;lt;Connection&amp;gt; readyConnections = findReadyConnections(connections);

            // 이벤트 처리
            for (Connection connection : readyConnections) {
                connection.handle();
            }
        }
    }

    private static List&amp;lt;Connection&amp;gt; findReadyConnections(List&amp;lt;Connection&amp;gt; connections) {

        return connections.stream()
                .filter(Connection::hasEvent)
                .toList();
    }
}

class Connection {

    private final String name;

    Connection(String name) {
        this.name = name;
    }

    boolean hasEvent() {
        // 실제로는 epoll, kqueue 등이 이벤트를 알려준다.
        return Math.random() &amp;gt; 0.7;
    }

    void handle() {
        System.out.println(name + &quot; 요청 처리&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 루프는 매우 단순한 구조입니다.&lt;/p&gt;
&lt;p data-end=&quot;1322&quot; data-start=&quot;1201&quot; data-ke-size=&quot;size16&quot;&gt;먼저 이벤트가 발생한 연결을 확인한 뒤(findReadyConnections()), 이벤트가 존재하는 연결만 순회하며 처리합니다(handle()). 이후 다시 이벤트를 확인하고 처리하는 과정을 계속 반복합니다. 실제 운영체제에서는 findReadyConnections() 역할을 epoll, kqueue와 같은 I/O 멀티플렉싱 기술이 수행합니다. 이벤트 루프는 전달받은 이벤트만 처리하면서 계속 반복 실행됩니다. Redis의 이벤트 루프도 개념적으로는 이 구조와 거의 같습니다. 다만 실제 구현에서는 epoll_wait() 등이 이벤트가 발생할 때까지 블로킹하며 대기하고, 이벤트가 발생하면 해당 이벤트 목록만 전달받아 처리하는 방식으로 동작합니다.&lt;/p&gt;
&lt;p data-end=&quot;445&quot; data-start=&quot;322&quot; data-ke-size=&quot;size16&quot;&gt;여기서는 epoll을 &lt;b&gt;&quot;이벤트가 발생했음을 알려주는 알람 기능&quot;&lt;/b&gt; 정도로 이해해도 충분합니다. 내부 동작은 훨씬 복잡하지만, 이벤트 루프가 필요한 연결만 처리할 수 있도록 도와주는 역할을 한다고 생각하면 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이벤트 기반 서버를 구현하는 대표적인 I/O 처리 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이벤트 기반 서버를 구현하는 대표적인 I/O 처리 패턴은 &lt;/span&gt;&lt;span&gt;Reactor&lt;/span&gt;&lt;span&gt;와 &lt;/span&gt;&lt;span&gt;Proactor&lt;/span&gt;&lt;span&gt;, 크게 두 가지입니다. &lt;/span&gt;&lt;span&gt;앞에서 살펴본 코드는 이벤트 루프의 동작 원리를 이해하기 위해 단순화한 예제입니다. 실제 시스템에서는 이벤트를 감지하고 처리하는 방식에 따라 Reactor와 Proactor 패턴으로 구현됩니다. &lt;/span&gt;&lt;span&gt;두 패턴 모두 이벤트 기반으로 동작한다는 공통점이 있지만, &lt;/span&gt;&lt;span&gt;누가 I/O를 수행하고 이벤트를 처리하는지&lt;/span&gt;&lt;span&gt;에 따라 동작 방식에 차이가 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;이 글에서는 &lt;/span&gt;&lt;span&gt;Reactor 패턴&lt;/span&gt;&lt;span&gt;을 중심으로 설명하겠습니다. 그 이유는 Reactor 패턴이 Redis를 비롯해 Nginx, Netty와 같은 네트워크 서버에서 널리 사용되는 이벤트 처리 방식이기 때문입니다. &lt;/span&gt;&lt;/span&gt;&lt;span&gt;반면 Proactor 패턴도 이벤트 기반으로 동작하지만, 주로 운영체제가 비동기 I/O를 직접 지원하는 환경에서 사용됩니다. 따라서 Redis의 동작 방식을 이해하기 위해서는 Reactor 패턴을 먼저 이해하는 것이 가장 자연스럽습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Reactor&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor Pattern은 이벤트가 발생할 때까지 기다렸다가(Event Demultiplexing), 이벤트가 발생하면 해당 이벤트를 적절한 처리 로직(Handler)으로 전달하는 이벤트 기반 설계 패턴입니다. 쉽게 말하면 &quot;이벤트를 감지하고, 적절한 담당자에게 일을 분배하는 패턴&quot;이라고 생각하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1783099154173&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while (true) {

    List&amp;lt;Event&amp;gt; events = findReadyEvents();

    for (Event event : events) {
        reactor.dispatch(event);
    }
}

public void dispatch(Event event) {

    switch (event.getType()) {
        case GET -&amp;gt; get(event);
        case SET -&amp;gt; set(event);
        case DEL -&amp;gt; del(event);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;347&quot; data-start=&quot;267&quot; data-ke-size=&quot;size16&quot;&gt;이벤트 루프가 계속 실행되면서 이벤트를 기다리고 있다가, 이벤트가 발생하면 어떤 작업을 수행해야 하는지 확인한 뒤 해당 처리 로직을 호출합니다.&lt;/p&gt;
&lt;p data-end=&quot;347&quot; data-start=&quot;267&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 클라이언트의 요청을 적절한 명령 처리 로직으로 전달하기 위해 Reactor 패턴을 사용합니다.&lt;/p&gt;
&lt;h2 data-end=&quot;347&quot; data-start=&quot;267&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 글에서는 &lt;/span&gt;&lt;span&gt;Redis가 싱글 스레드임에도 높은 성능을 낼 수 있는 이유&lt;/span&gt;&lt;span&gt;를 C10K 문제부터 I/O 멀티플렉싱, 이벤트 루프, 그리고 Reactor 패턴까지 살펴보며 알아보았습니다. &lt;/span&gt;&lt;span&gt;Redis가 단순히 &quot;싱글 스레드라서 빠르다&quot;가 아니라, C10K 문제를 해결하기 위해 등장한 기술과 설계 방식을 적극적으로 활용한 결과라는 점을 이해할 수 있었습니다. &lt;/span&gt;&lt;span&gt;다만 이번 글에서는 주제에서 벗어나지 않기 위해 Reactor 패턴의 내부 동작이나 구현 방식까지는 자세히 다루지 않았습니다. Redis의 동작 원리를 이해하는 데 필요한 수준에서만 살펴보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음 글에서는 Reactor 패턴을 조금 더 깊이 있게 살펴보고, 또 다른 이벤트 처리 방식인 &lt;/span&gt;&lt;span&gt;Proactor 패턴&lt;/span&gt;&lt;span&gt;과의 차이점도 함께 알아보겠습니다. 두 패턴은 모두 이벤트 기반으로 동작하지만 I/O를 처리하는 방식에 차이가 있으며, 어떤 환경에서 각각이 적합한지도 함께 살펴볼 예정입니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/587</guid>
      <comments>https://b-programmer.tistory.com/587#entry587comment</comments>
      <pubDate>Sat, 4 Jul 2026 02:33:34 +0900</pubDate>
    </item>
    <item>
      <title>경쟁조건을 해결하거나 완화하기 위한 다양한 기술들</title>
      <link>https://b-programmer.tistory.com/586</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&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;&amp;nbsp;을 사용하는 것입니다. 락을 사용하면 하나의 요청이 공유 자원을 사용하는 동안 다른 요청들은 해당 자원에 접근하지 못하고 대기하게 됩니다. 이후 현재 작업이 모두 끝나고 락이 해제되면 다음 요청이 자원을 사용할 수 있으므로 경쟁 조건을 방지할 수 있습니다. &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;/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-end=&quot;392&quot; data-start=&quot;298&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 재고가 100개인 상품이 있다고 가정해 보겠습니다. 100개의 스레드가 동시에 재고를 1개씩 차감한다면, 정상적인 결과는 재고가 0개가 되는 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;523&quot; data-start=&quot;397&quot; data-ke-size=&quot;size16&quot;&gt;하지만 경쟁 조건이 발생하면 여러 스레드가 동일한 재고 값을 동시에 읽고 수정하면서 일부 차감 연산이 반영되지 않을 수 있습니다. 실제로 테스트해 보면 재고가 87개와 같이 예상과 다른 결과가 나오는 경우도 발생합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;54&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q1w5o/dJMcacwSvOO/Qd0PUzEm5XPMKpx7tJuFbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q1w5o/dJMcacwSvOO/Qd0PUzEm5XPMKpx7tJuFbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q1w5o/dJMcacwSvOO/Qd0PUzEm5XPMKpx7tJuFbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq1w5o%2FdJMcacwSvOO%2FQd0PUzEm5XPMKpx7tJuFbk%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;394&quot; height=&quot;50&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;54&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 현상을 경쟁 조건(Race Condition) 이라고 합니다. 여러 스레드나 서버가 동시에 동일한 자원에 접근하면서 서로의 작업이 충돌하여, 예상했던 결과와 다른 값이 발생하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 경쟁 조건을 해결하기 위해 가장 널리 사용되는 방법 중 하나인 &lt;b&gt;Lock&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;426&quot; data-origin-height=&quot;54&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zmOkB/dJMb991fkNB/iTxvQUHQytmfpNKN8iUIGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zmOkB/dJMb991fkNB/iTxvQUHQytmfpNKN8iUIGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zmOkB/dJMb991fkNB/iTxvQUHQytmfpNKN8iUIGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzmOkB%2FdJMb991fkNB%2FiTxvQUHQytmfpNKN8iUIGK%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;394&quot; height=&quot;50&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;54&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Lock을 사용하면 하나의 요청이 작업을 수행하는 동안 다른 요청들은 대기해야 하므로, 동시성이 낮아지고 성능이 저하될 수 있습니다. 요청 수는 10만건으로 테스트를 진행해보겠습니다.&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;674&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qDT1h/dJMcajvZXfz/Z8LXqhf1uBX4HTuDhPd52K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qDT1h/dJMcajvZXfz/Z8LXqhf1uBX4HTuDhPd52K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qDT1h/dJMcajvZXfz/Z8LXqhf1uBX4HTuDhPd52K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqDT1h%2FdJMcajvZXfz%2FZ8LXqhf1uBX4HTuDhPd52K%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;320&quot; height=&quot;393&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10만 건의 요청을 기준으로 테스트한 결과, 전체 처리 시간은 59.97초였으며 처리량은 약 1,667 req/s를 기록했습니다. 또한 평균 응답 시간은 0.600ms로 측정되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직까지는 Lock이 실제로 성능을 얼마나 저하시켰는지는 판단하기 어려웠습니다. 다만 10만 건의 요청을 처리하는 데 약 1분이 소요된 것을 보면, 개선의 여지가 있지 않을까 하는 생각이 들었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lua script&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lock만이 경쟁 조건을 해결하는 유일한 방법은 아닙니다. Lock을 사용하지 않고도 경쟁 조건을 해결하거나 완화할 수 있는 방법이 있으며, 그중 하나가 &lt;b&gt;Lua Script&lt;/b&gt;입니다. 이번에는 Lock 대신 &lt;b&gt;Lua Script&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;490&quot; data-origin-height=&quot;60&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d58qLR/dJMcabxVlMj/AnXnQh2CgWD1MaYtmqaewk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d58qLR/dJMcabxVlMj/AnXnQh2CgWD1MaYtmqaewk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d58qLR/dJMcabxVlMj/AnXnQh2CgWD1MaYtmqaewk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd58qLR%2FdJMcabxVlMj%2FAnXnQh2CgWD1MaYtmqaewk%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;384&quot; height=&quot;47&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;60&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 10만건을 대상으로 진행하면 어떻게 되는지 Lock과 비교를 해봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caDjKR/dJMcabSdyoP/f4lu8CLul3tsxZokKp4nnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caDjKR/dJMcabSdyoP/f4lu8CLul3tsxZokKp4nnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caDjKR/dJMcabSdyoP/f4lu8CLul3tsxZokKp4nnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaDjKR%2FdJMcabSdyoP%2Ff4lu8CLul3tsxZokKp4nnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;609&quot; height=&quot;374&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소요 시간이 20배 가까이 빨라졌다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면 왜 이렇게 빠를까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락은 일반적으로 락 획득 &amp;rarr; 비즈니스 로직 수행 &amp;rarr; 락 해제 과정을 거칩니다. 이 과정에서 Redis와 여러 번 통신해야 하므로 네트워크 왕복(RTT)이 반복적으로 발생하고, 락을 획득하기 위해 대기하는 시간도 생길 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;365&quot; data-start=&quot;206&quot; data-ke-size=&quot;size16&quot;&gt;반면 Lua Script는 여러 Redis 명령을 하나의 스크립트로 묶어 원자적으로 실행합니다. 따라서 클라이언트는 Redis와 한 번의 네트워크 왕복(RTT) 만 수행하면 되고, 스크립트 내부에서는 다른 요청이 개입하지 않으므로 경쟁 조건도 방지할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kX1i5/dJMcabrcI3p/I1CIpC76hZq0NKaBcTb7R1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kX1i5/dJMcabrcI3p/I1CIpC76hZq0NKaBcTb7R1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kX1i5/dJMcabrcI3p/I1CIpC76hZq0NKaBcTb7R1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkX1i5%2FdJMcabrcI3p%2FI1CIpC76hZq0NKaBcTb7R1%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;556&quot; height=&quot;271&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Redis를 단일 데이터 저장소로 사용하는 경우에는 DB를 별도로 조회하거나 갱신하는 과정이 줄어들기 때문에 성능 향상에 더욱 큰 영향을 줄 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;704&quot; data-start=&quot;468&quot; data-ke-size=&quot;size16&quot;&gt;하지만 그렇다고 해서 Lua Script가 항상 정답은 아닙니다. Redis가 단일 진실 소스(Single Source of Truth) 가 되는 만큼, 데이터의 영속성이나 감사(Audit)가 필요한 서비스라면 DB와의 동기화 전략도 함께 고려해야 합니다. 예를 들어 CDC(Change Data Capture) 나 비동기 이벤트를 이용하여 Redis와 DB의 데이터를 동기화하는 방식을 선택할 수 있습니다.&lt;/p&gt;
&lt;h4 data-end=&quot;782&quot; data-start=&quot;709&quot; data-ke-size=&quot;size20&quot;&gt;그렇다면 Lua Script로 변경하는 것이 항상 좋은 선택일까요?&lt;/h4&gt;
&lt;p data-end=&quot;782&quot; data-start=&quot;709&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가장 큰 단점은 &lt;/span&gt;&lt;span&gt;경쟁 조건을 해결하는 로직이 애플리케이션이 아닌 Lua Script에 작성된다는 점&lt;/span&gt;&lt;span&gt;입니다. 따라서 Java 코드만으로는 전체 동작을 파악하기 어렵고, 디버깅이나 테스트도 상대적으로 까다로워질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한 Redis는 단일 스레드 기반으로 Lua Script를 실행하기 때문에, 스크립트의 실행 시간이 길어질수록 다른 요청들도 함께 대기하게 됩니다. 따라서 복잡한 비즈니스 로직을 Lua Script에 작성하기보다는, &lt;/span&gt;&lt;span&gt;짧고 원자적인 작업만 수행하도록 구성하는 것이 좋습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;정렬 집합&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Lock이나 Lua Script를 사용하지 않고도 경쟁 조건을 해결하거나 완화할 수 있는 방법은 없을까요? 이번에 살펴볼 방법은 &lt;/span&gt;&lt;b&gt;&lt;span&gt;정렬 집합(Sorted Set)&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 을 활용하는 방식입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기본적인 아이디어는 단순합니다. 요청을 일정한 기준으로 정렬하여 관리함으로써 동일한 자원에 대한 충돌을 최소화하는 것입니다. 이를 통해 복잡한 락을 사용하지 않고도 경쟁 조건을 완화할 수 있으며, 특히 시간 순서가 중요한 데이터나 요청을 처리할 때 효과적으로 활용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;54&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YGDSg/dJMcaiX4GJh/xD8EZkMot4Ycjk0noMMkPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YGDSg/dJMcaiX4GJh/xD8EZkMot4Ycjk0noMMkPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YGDSg/dJMcaiX4GJh/xD8EZkMot4Ycjk0noMMkPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYGDSg%2FdJMcaiX4GJh%2FxD8EZkMot4Ycjk0noMMkPk%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;508&quot; height=&quot;54&quot; data-origin-width=&quot;508&quot; data-origin-height=&quot;54&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상과 달리 정렬 집합만 했을뿐인데도 재고는 0으로 찍혔다는것을 알 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;836&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdvTyc/dJMcacXVQKx/pybL5Bjbs2yrMMUr5GNogK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdvTyc/dJMcacXVQKx/pybL5Bjbs2yrMMUr5GNogK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdvTyc/dJMcacXVQKx/pybL5Bjbs2yrMMUr5GNogK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdvTyc%2FdJMcacXVQKx%2FpybL5Bjbs2yrMMUr5GNogK%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;618&quot; height=&quot;247&quot; data-origin-width=&quot;836&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;엄청난 성능 향상이 있었던 것은 아니지만, 이전 방식보다 준수한 성능을 보여주었습니다. 특히 &lt;/span&gt;&lt;span&gt;10만 건의 요청을 기준으로 했을 때도 Lua Script와의 차이는 약 3초 정도에 불과했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한 Lua Script와 달리 &lt;/span&gt;&lt;span&gt;순수하게 Java 코드만으로 구현할 수 있다는 점&lt;/span&gt;&lt;span&gt;도 큰 장점이라고 생각합니다. 별도의 스크립트를 관리할 필요가 없기 때문에 디버깅과 테스트가 비교적 수월하며, 유지보수 측면에서도 부담이 적습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YnMpP/dJMcab5Nu2x/FBdiCug2gtccQxWoyxaQm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YnMpP/dJMcab5Nu2x/FBdiCug2gtccQxWoyxaQm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YnMpP/dJMcab5Nu2x/FBdiCug2gtccQxWoyxaQm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYnMpP%2FdJMcab5Nu2x%2FFBdiCug2gtccQxWoyxaQm0%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;1522&quot; height=&quot;140&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 한 가지 의문이 생깁니다. &lt;/span&gt;&lt;span&gt;단순히 정렬만 했을 뿐인데 어떻게 경쟁 조건을 해결할 수 있었던 걸까요?&lt;/span&gt;&lt;span&gt; 정렬 집합은 어떤 원리로 경쟁 조건을 완화하는지 함께 살펴보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정렬 집합은 어떻게 경쟁 조건을 완화할 수 있을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 시간(timestamp)을 기준으로 정렬한다면, 각 요청은 발생한 시점에 따라 순서가 부여됩니다. 이렇게 되면 요청의 순서를 쉽게 파악할 수 있고, 특정 범위의 데이터를 조회하거나 오래된 데이터를 제거하는 작업도 효율적으로 수행할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2118&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mcU3U/dJMcaccyJhS/Rvoakke4SvJ5JQ78ilVgs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mcU3U/dJMcaccyJhS/Rvoakke4SvJ5JQ78ilVgs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mcU3U/dJMcaccyJhS/Rvoakke4SvJ5JQ78ilVgs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmcU3U%2FdJMcaccyJhS%2FRvoakke4SvJ5JQ78ilVgs0%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;2118&quot; height=&quot;334&quot; data-origin-width=&quot;2118&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, 정렬 집합은 &lt;/span&gt;&lt;span&gt;여러 요청이 하나의 값을 반복해서 수정하는 구조가 아니라, 각 요청을 독립적인 데이터로 관리할 수 있도록 도와주는 자료구조&lt;/span&gt;&lt;span&gt;입니다. 이러한 특성 덕분에 데이터 충돌이 발생하기 쉬운 구조를 개선할 수 있으며, 경쟁 조건을 완화하는 데 활용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;784&quot; data-start=&quot;766&quot; data-ke-size=&quot;size16&quot;&gt;사실, 경쟁 조건을 해결할 수 있었던 것은 정렬 집합이 아니라 Redis의 특성 때문입니다. Redis는 싱글 스레드로 동작합니다. 즉, 하나의 요청을 처리하는 동안 다른 요청은 순차적으로 대기하게 됩니다. 이러한 특성 덕분에 요청이 순차적으로 처리되어 경쟁 조건을 해결할 수 있습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;784&quot; data-start=&quot;766&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 학습을 통해 경쟁 조건은 반드시 Lock만으로 해결해야 하는 문제가 아니라는 것을 알게 되었습니다. 대표적으로 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Lua Script&lt;/span&gt;&lt;/b&gt;&lt;span&gt;와 &lt;/span&gt;&lt;b&gt;&lt;span&gt;정렬 집합(Sorted Set)&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 을 이용해서도 경쟁 조건을 해결하거나 완화할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 두 방법의 핵심은 &lt;/span&gt;&lt;b&gt;&lt;span&gt;원자성(Atomicity)&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 입니다. 원자성이란 하나의 작업을 중간에 다른 요청이 개입할 수 없는 하나의 작업 단위로 처리하는 특성을 의미합니다. 예를 들어 추가와 삭제 작업을 하나의 작업 단위로 처리함으로써 경쟁 조건을 방지할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;성능만 비교한다면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Lua Script&lt;/span&gt;&lt;/b&gt;&lt;span&gt;가 가장 좋은 결과를 보여주었습니다. 다만 원자성을 보장하는 로직이 Java 코드가 아닌 Lua Script에 작성되기 때문에, 문제가 발생했을 경우 디버깅이나 유지보수가 다소 어려울 수 있습니다. 따라서 Lua Script는 스크립트만으로도 동작을 쉽게 이해할 수 있을 정도의 단순한 로직에 사용하는 것이 적합하다고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;정렬 집합&lt;/span&gt;&lt;/b&gt;&lt;span&gt;은 Lua Script보다는 성능이 다소 떨어졌지만 충분히 준수한 성능을 보여주었습니다. 또한 Java 코드만으로 구현할 수 있다는 점도 장점이라고 생각합니다. 따라서 구현해야 하는 로직이 다소 복잡하거나 Java 코드 중심으로 관리하고 싶다면 정렬 집합도 충분히 좋은 선택지가 될 수 있다고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그렇다면 Lock은 사용하지 말아야 할까요? 그렇지는 않습니다. Lock은 경쟁 조건을 가장 확실하게 방지할 수 있는 방법 중 하나입니다. 다만 다른 방식에 비해 성능 저하가 발생할 수 있으므로, 실시간 처리가 중요한 환경에서는 신중하게 사용하는 것이 좋다고 생각합니다. 반대로 배치 작업이나 스케줄링처럼 처리 시간이 다소 길어져도 문제가 없는 환경에서는 Lock이 가장 적합한 선택이 될 수 있다고 생각합니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/586</guid>
      <comments>https://b-programmer.tistory.com/586#entry586comment</comments>
      <pubDate>Mon, 29 Jun 2026 08:54:55 +0900</pubDate>
    </item>
    <item>
      <title>Rate Limiter (intro.)</title>
      <link>https://b-programmer.tistory.com/585</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;수많은 사용자들이 동시에 서비스에 접근하게 되면 서비스가 정상적으로 동작하지 못하는 상황이 발생할 수 있습니다. 특히 특정 사용자가 과도한 요청을 보내거나 예상보다 많은 트래픽이 유입되는 경우, 서버의 자원이 빠르게 소모되어 응답 지연이나 장애로 이어질 수 있습니다.&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이러한 문제를 방지하기 위해 서비스는 사용할 수 있는 자원을 적절히 제어해야 하며, 그중 가장 대표적인 방법이 Rate Limiter입니다. Rate Limiter는 일정 시간 동안 허용되는 요청 수를 제한하여 서비스의 안정성을 보장하고, 모든 사용자에게 공정한 사용 환경을 제공하는 역할을 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;350&quot; data-start=&quot;183&quot; data-ke-size=&quot;size26&quot;&gt;Rate Limiter를 왜 사용해야 할까? 대안은 없을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본론적으로 생각해보면 API 요청에 굳이 제한을 두어야 하는지 의문이 들 수 있습니다. 캐시를 활용하여 응답 속도를 개선하거나, 큐를 이용해 요청을 순차적으로 처리하고, 서버를 증설하여 처리량을 늘리는 방법도 존재하기 때문입니다. 이러한 방법들만으로도 충분히 트래픽 문제를 해결할 수 있다고 생각할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;528&quot; data-start=&quot;309&quot; data-ke-size=&quot;size16&quot;&gt;또한 Rate Limiter를 적용하면 모든 요청에 대해 제한 여부를 확인해야 하므로 API 성능에 영향을 주지 않을까 걱정될 수도 있습니다. 하지만 일반적으로 Rate Limiter가 성능 저하의 원인이 될 정도라면 설계 자체에 문제가 있을 가능성이 높습니다. 대부분의 Rate Limiter는 메모리 기반 저장소를 활용하거나 단순한 카운팅 연산만 수행하기 때문에 오버헤드가 크지 않습니다.&lt;/p&gt;
&lt;p data-end=&quot;663&quot; data-start=&quot;533&quot; data-ke-size=&quot;size16&quot;&gt;물론 캐시, 큐, 서버 증설과 같은 방법은 시스템의 처리 능력을 높이거나 부하를 분산시키는 데 효과적입니다. 하지만 이러한 방법들은 시스템이 더 많은 요청을 처리할 수 있도록 돕는 역할일 뿐, 과도한 요청 자체를 제어하지는 못합니다.&lt;/p&gt;
&lt;p data-end=&quot;861&quot; data-start=&quot;668&quot; data-ke-size=&quot;size16&quot;&gt;반면 Rate Limiter는 시스템이 감당할 수 있는 수준 이상의 요청을 사전에 제한함으로써 서비스 전체를 보호합니다. 따라서 Rate Limiter는 캐시나 큐, 서버 증설을 대체하는 기술이 아니라, 예기치 못한 트래픽 증가나 비정상적인 요청으로부터 시스템을 보호하기 위한 안정장치(Safety Net)로 이해하는 것이 적절하다고 생각합니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면, Rate Limiter는 어느 위치에 둬야할까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rate Limiter는 크게 클라이언트, 서버, 그리고 미들웨어 계층에 배치할 수 있습니다. 어느 위치에 배치하느냐에 따라 목적과 효과가 조금씩 달라집니다. 예를 들어, 클라이언트 측에 Rate Limiter를 적용하면 사용자는 요청 제한 여부를 즉시 확인할 수 있으므로 사용자 경험(UX) 측면에서 유리합니다. 또한 중복 클릭이나 짧은 시간 내 반복 요청을 방지하여 불필요한 네트워크 트래픽을 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;613&quot; data-start=&quot;497&quot; data-ke-size=&quot;size16&quot;&gt;하지만 클라이언트는 사용자가 제어할 수 있는 영역입니다. 브라우저의 개발자 도구를 이용하거나 직접 API를 호출하는 방식으로 제한을 우회할 수 있기 때문에, 보안이나 서버 보호 관점에서는 신뢰하기 어렵습니다. 따라서, 클라이언트 측 Rate Limiter는 서버를 보호하기 위한 수단이라기보다는 사용자 경험을 개선하고 불필요한 요청을 줄이기 위한 보조적인 수단으로 활용됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그렇다면, 서버에 Rate Limiter를 두는 이유는 무엇일까요?&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 이유는 서버가 신뢰할 수 있는 영역이기 때문입니다. 클라이언트 측에 적용된 Rate Limiter는 사용자가 직접 수정하거나 우회할 수 있지만, 서버 측 Rate Limiter는 모든 요청이 반드시 거쳐야 하는 지점에서 동작하므로 우회가 어렵습니다. 또한, 서버는 전체 트래픽 현황을 파악할 수 있습니다. 특정 사용자가 짧은 시간 동안 과도한 요청을 보내고 있는지, 특정 IP에서 비정상적인 트래픽이 발생하고 있는지, 혹은 서비스 전체가 처리 가능한 한계를 초과하고 있는지 등을 판단할 수 있습니다. 무엇보다 서버 측 Rate Limiter는 애플리케이션 로직이 수행되기 전에 요청을 차단할 수 있다는 장점이 있습니다. 만약 제한 없이 모든 요청을 애플리케이션까지 전달한다면 CPU, 메모리, 데이터베이스 커넥션과 같은 자원이 불필요하게 소모될 수 있습니다. 반면 Rate Limiter를 통해 초기에 요청을 거부하면 시스템 자원을 보호하고 서비스 전체의 안정성을 유지할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;727&quot; data-start=&quot;624&quot; data-ke-size=&quot;size16&quot;&gt;따라서 클라이언트 측 Rate Limiter가 사용자 경험 향상을 위한 보조 수단이라면, 서버 측 Rate Limiter는 시스템을 보호하기 위한 핵심 방어 수단이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;727&quot; data-start=&quot;624&quot; data-ke-size=&quot;size16&quot;&gt;추가적으로 애플리케이션 서버 앞단에 위치한 게이트웨이를 통해 Rate Limiter를 적용할 수도 있습니다. 게이트웨이는 클라이언트의 요청을 가장 먼저 수신하는 계층으로, 인증, 로깅, 라우팅, Rate Limiting과 같은 공통 기능을 처리하는 역할을 담당합니다.&lt;/p&gt;
&lt;p data-end=&quot;459&quot; data-start=&quot;325&quot; data-ke-size=&quot;size16&quot;&gt;Rate Limiter를 게이트웨이에 배치하면 애플리케이션 서버에 요청이 전달되기 전에 제한 여부를 판단할 수 있습니다. 이를 통해 불필요한 요청이 서버까지 도달하는 것을 방지하고, 애플리케이션의 자원을 더욱 효율적으로 보호할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;620&quot; data-start=&quot;464&quot; data-ke-size=&quot;size16&quot;&gt;실제로 스프링 생태계에서는 &lt;span&gt;&lt;span&gt;Spring Cloud Gateway&lt;/span&gt;&lt;/span&gt;를 제공하며, Redis 기반의 Rate Limiting 기능을 지원합니다. 이를 활용하면 별도의 Rate Limiter를 직접 구현하지 않고도 요청 수를 효과적으로 제어할 수 있습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;727&quot; data-start=&quot;624&quot; data-ke-size=&quot;size26&quot;&gt;그렇다면, 단순히 요청을 count하면 되지 않을까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 생각하면 API 요청 횟수를 데이터베이스에 저장하고, 요청이 들어올 때마다 조회하여 제한 여부를 판단하면 될 것 같습니다. 하지만 Rate Limiter를 설계할 때 가장 중요한 점은 Rate Limiter 자체가 시스템에 큰 부하를 주어서는 안 된다는 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;229&quot; data-ke-size=&quot;size16&quot;&gt;만약 모든 API 요청마다 데이터베이스에 접근하여 요청 횟수를 조회하고 갱신한다면, 트래픽이 증가할수록 데이터베이스에 과도한 부하가 발생할 수 있습니다. 극단적인 경우에는 서비스 로직보다 Rate Limiter가 더 많은 자원을 소비하는 상황이 발생할 수도 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;526&quot; data-start=&quot;383&quot; data-ke-size=&quot;size16&quot;&gt;따라서 Rate Limiter는 요청을 제한하는 기능뿐만 아니라, 매우 빠른 속도로 동작해야 한다는 요구사항도 함께 만족해야 합니다. 이상적인 Rate Limiter는 요청 처리에 미치는 영향을 최소화하면서도 정확하게 요청 횟수를 제어할 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 데이터베이스의 부하를 최소화하면서도 효과적으로 요청 수를 관리할 수 있는 방법은 무엇일까요?&lt;br /&gt;Rate Limiter를 구현하기 위한 알고리즘은 다양하지만, 큰 관점에서 보면 버킷(Bucket) 기반 알고리즘과 윈도우(Window) 기반 알고리즘으로 구분할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;버킷 기반 알고리즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버킷(Bucket)을 바구니라고 생각해봅시다. 그리고 바구니 안에는 &lt;u&gt;요청을 처리할 수 있는 권한인 토큰&lt;/u&gt;(Token)이 들어있다고 가정하겠습니다.&lt;/p&gt;
&lt;p data-end=&quot;371&quot; data-start=&quot;260&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 버킷 기반 알고리즘은 크게 두 가지 관점으로 생각해볼 수 있습니다. 첫 번째는 &lt;u&gt;일정한 속도로 버킷에 토큰을 채우는 방법&lt;/u&gt;이고, 두 번째는 &lt;u&gt;요청이 발생할 때 버킷에서 토큰을 소비하는 방법&lt;/u&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;536&quot; data-start=&quot;376&quot; data-ke-size=&quot;size16&quot;&gt;결국 버킷 기반 알고리즘은 토큰을 어떻게 채우고, 어떻게 소비하느냐에 따라 다양한 형태로 구현될 수 있습니다. 대표적으로 Token Bucket은 요청이 발생할 때 토큰을 소비하는 방식으로 동작하며, Leaky Bucket은 일정한 속도로 요청을 처리하도록 제어하는 방식으로 동작합니다.&lt;/p&gt;
&lt;h4 data-end=&quot;536&quot; data-start=&quot;376&quot; data-ke-size=&quot;size20&quot;&gt;Token Bucket&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 해당하는 그림을 그려보면 다음과 같이 그릴 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chj1zE/dJMcajvZdpz/3xGDVtBHivBKS0rVHOR3v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chj1zE/dJMcajvZdpz/3xGDVtBHivBKS0rVHOR3v0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chj1zE/dJMcajvZdpz/3xGDVtBHivBKS0rVHOR3v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fchj1zE%2FdJMcajvZdpz%2F3xGDVtBHivBKS0rVHOR3v0%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;477&quot; height=&quot;347&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰은 일정한 주기에 따라 버킷에 충전됩니다. 그리고 API 요청이 들어오면 버킷에 저장된 토큰을 하나 소비하여 요청을 처리합니다.&lt;/p&gt;
&lt;p data-end=&quot;429&quot; data-start=&quot;347&quot; data-ke-size=&quot;size16&quot;&gt;하지만 버킷에 사용할 수 있는 토큰이 남아 있지 않다면 요청은 거부되며, 일반적으로 HTTP 429(Too Many Requests)를 반환합니다.&lt;/p&gt;
&lt;p data-end=&quot;623&quot; data-start=&quot;434&quot; data-ke-size=&quot;size16&quot;&gt;이 알고리즘에서 가장 중요한 것은 &lt;b&gt;토큰의 충전 속도(Refill Rate)&lt;/b&gt; 와 &lt;b&gt;버킷의 최대 크기(Capacity)&lt;/b&gt; 를 어떻게 설정할 것인가입니다. 예를 들어, 초당 2개의 토큰을 충전하는 경우와 2초마다 1개의 토큰을 충전하는 경우는 장기적인 처리량은 비슷할 수 있지만, 순간적인 요청을 처리하는 방식에는 차이가 발생합니다.&lt;/p&gt;
&lt;p data-end=&quot;773&quot; data-start=&quot;628&quot; data-ke-size=&quot;size16&quot;&gt;또한, 버킷의 크기가 크다면 평소 사용하지 않은 토큰을 저장해 두었다가 순간적으로 많은 요청(Burst Traffic)이 발생했을 때 이를 처리할 수 있습니다. 반대로 버킷의 크기가 작거나 충전 속도가 너무 느리다면 정상적인 사용자도 요청이 제한될 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;869&quot; data-start=&quot;778&quot; data-ke-size=&quot;size16&quot;&gt;따라서 Token Bucket 알고리즘은 토큰을 얼마나 빠르게 충전할 것인지와 버킷의 크기를 어떻게 설정할 것인지가 성능을 결정하는 핵심 요소라고 볼 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Leaky Bucket&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Token Bucket은 버킷에 토큰을 일정한 속도로 충전(Refill)하고, 요청이 들어올 때마다 토큰을 소비하여 처리율을 제한하는 방식입니다.&lt;/p&gt;
&lt;p data-end=&quot;218&quot; data-start=&quot;114&quot; data-ke-size=&quot;size16&quot;&gt;반면 Leaky Bucket은 요청을 버킷에 저장한 뒤, 일정한 속도로 요청을 처리합니다. 버킷의 용량을 초과하는 요청이 들어오면 더 이상 저장할 수 없기 때문에 해당 요청은 거부됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;716&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cI9w3a/dJMcadvFtRs/s7s69zngYwtx6KsIaw9Cg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cI9w3a/dJMcadvFtRs/s7s69zngYwtx6KsIaw9Cg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cI9w3a/dJMcadvFtRs/s7s69zngYwtx6KsIaw9Cg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcI9w3a%2FdJMcadvFtRs%2Fs7s69zngYwtx6KsIaw9Cg0%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;556&quot; height=&quot;260&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;716&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-is-last-node=&quot;&quot; data-end=&quot;289&quot; data-start=&quot;135&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로 Leaky Bucket은 바리게이트와 비슷한 방식이라고 생각합니다. 한 번에 많은 요청이 몰리더라도 모두를 처리하는 것이 아니라, 미리 정해진 속도로만 요청을 통과시킵니다. 이를 통해 갑작스러운 트래픽 증가에도 시스템이 일정한 처리율을 유지할 수 있도록 도와줍니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;289&quot; data-start=&quot;135&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 언제 Token Bucket을 선택하고, 언제 Leaky Bucket을 선택해야 할까요? 두 알고리즘 모두 요청을 제한한다는 공통점이 있지만, 트래픽을 처리하는 방식은 분명한 차이가 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;289&quot; data-start=&quot;135&quot; data-ke-size=&quot;size16&quot;&gt;순간적으로 많은 요청을 허용하면서도 평균 처리율을 제한하고 싶다면 Token Bucket이 적합하고, 요청을 항상 일정한 속도로 처리하여 시스템의 안정성을 우선시한다면 Leaky Bucket이 적합하다고 생각합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;289&quot; data-start=&quot;135&quot; data-ke-size=&quot;size23&quot;&gt;윈도우 기반 알고리즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도우 기반 알고리즘을 이해하기 위해서는 먼저 &lt;b&gt;윈도우(Window)&lt;/b&gt; 라는 개념을 이해할 필요가 있습니다. 지금까지 살펴본 버킷 기반 알고리즘과 달리, 윈도우 기반 알고리즘은 &lt;b&gt;시간을 일정한 구간(Window)으로 나누어 요청을 관리&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-end=&quot;384&quot; data-start=&quot;201&quot; data-ke-size=&quot;size16&quot;&gt;윈도우 기반 알고리즘은 시간 구간을 어떻게 관리하느냐에 따라 여러 방식으로 나뉩니다. 대표적으로 &lt;b&gt;고정 윈도 카운터(Fixed Window Counter)&lt;/b&gt;, &lt;b&gt;이동 윈도 로깅(Sliding Window Log)&lt;/b&gt;, &lt;b&gt;이동 윈도 카운터(Sliding Window Counter)&lt;/b&gt; 의 세 가지 알고리즘이 있습니다.&lt;/p&gt;
&lt;h4 data-end=&quot;384&quot; data-start=&quot;201&quot; data-ke-size=&quot;size20&quot;&gt;고정 윈도 카운터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;윈도우가 시간을 일정 구간으로 나눠서 요청을 관리한다고 하였습니다. 그렇다면 &lt;b&gt;고정 윈도(Fixed Window)&lt;/b&gt; 는 이름 그대로 시간을 동일한 크기의 구간으로 나누어 요청을 관리하는 알고리즘입니다.&lt;/p&gt;
&lt;p data-end=&quot;395&quot; data-start=&quot;225&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 윈도우의 크기를 &lt;b&gt;1분&lt;/b&gt;으로 설정했다면, 시간은 00:00 ~ 00:01, 00:01 ~ 00:02, 00:02 ~ 00:03과 같이 고정된 구간으로 나뉘게 됩니다. 그리고 각 구간마다 요청 수를 별도로 계산하여, 설정한 임계치를 초과하는 경우 더 이상 요청을 처리하지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H78Dn/dJMcabScRfC/9pITiM86qEcbQOx3mkyCO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H78Dn/dJMcabScRfC/9pITiM86qEcbQOx3mkyCO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H78Dn/dJMcabScRfC/9pITiM86qEcbQOx3mkyCO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH78Dn%2FdJMcabScRfC%2F9pITiM86qEcbQOx3mkyCO0%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;539&quot; height=&quot;333&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 트래픽은 윈도우의 경계에 맞춰 발생하지 않습니다. 평소에는 요청이 적더라도 특정 시점에 많은 요청이 한꺼번에 몰릴 수 있으며, 이러한 특성 때문에 고정 윈도 카운터는 예상하지 못한 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1754&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGVVh2/dJMcahSpFYr/YCImk5rdW2aSPlWZp9KYfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGVVh2/dJMcahSpFYr/YCImk5rdW2aSPlWZp9KYfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGVVh2/dJMcahSpFYr/YCImk5rdW2aSPlWZp9KYfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGVVh2%2FdJMcahSpFYr%2FYCImk5rdW2aSPlWZp9KYfK%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;718&quot; height=&quot;213&quot; data-origin-width=&quot;1754&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 시점에 트래픽이 몰리는 상황을 가정해보겠습니다. Rate Limiter의 정책은 &lt;b&gt;1분 동안 최대 100개의 요청&lt;/b&gt;을 허용한다고 가정합니다.&lt;/p&gt;
&lt;p data-end=&quot;334&quot; data-start=&quot;187&quot; data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;00:00 ~ 01:00&lt;/b&gt; 구간의 마지막 20초인 &lt;b&gt;00:40&lt;/b&gt;부터 &lt;b&gt;80개의 요청&lt;/b&gt;이 발생했다고 해보겠습니다. 그리고 새로운 윈도우가 시작된 직후인 &lt;b&gt;01:00 ~ 01:40&lt;/b&gt; 사이에 &lt;b&gt;40개의 요청&lt;/b&gt;이 추가로 발생했다고 가정하겠습니다.&lt;/p&gt;
&lt;p data-end=&quot;370&quot; data-start=&quot;336&quot; data-ke-size=&quot;size16&quot;&gt;고정 윈도 카운터는 두 요청을 서로 다른 윈도우로 판단합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;423&quot; data-start=&quot;372&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;397&quot; data-start=&quot;372&quot; data-section-id=&quot;hk5rn5&quot;&gt;&lt;b&gt;00:00 ~ 01:00&lt;/b&gt; &amp;rarr; 80개&lt;/li&gt;
&lt;li data-end=&quot;423&quot; data-start=&quot;398&quot; data-section-id=&quot;1utnqpr&quot;&gt;&lt;b&gt;01:00 ~ 02:00&lt;/b&gt; &amp;rarr; 40개&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;486&quot; data-start=&quot;425&quot; data-ke-size=&quot;size16&quot;&gt;따라서 두 윈도우 모두 임계치인 100개를 넘지 않았기 때문에 Rate Limiter는 정상이라고 판단합니다.&lt;/p&gt;
&lt;p data-end=&quot;642&quot; data-start=&quot;488&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제 시간을 기준으로 보면 &lt;b&gt;00:40 ~ 01:40&lt;/b&gt; 사이에는 총 &lt;b&gt;120개의 요청&lt;/b&gt;이 발생한 것입니다. 즉, 매우 짧은 시간 동안 임계치를 초과하는 트래픽이 유입되었음에도, 고정된 시간 구간으로 나누어 계산했기 때문에 이를 감지하지 못하는 문제가 발생합니다.&lt;/p&gt;
&lt;p data-end=&quot;738&quot; data-start=&quot;644&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;윈도우의 경계(Boundary)&lt;/b&gt; 에서 발생하는 문제를 해결하기 위해 등장한 알고리즘이 &lt;b&gt;이동 윈도 로깅(Sliding Window Log)&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이동 윈도 로깅&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이동 윈도 로깅은 고정 윈도 카운터에서 발생하는 &lt;b&gt;경계 문제(Boundary Problem)&lt;/b&gt; 를 해결하기 위해 등장한 알고리즘입니다.&lt;/p&gt;
&lt;p data-end=&quot;292&quot; data-start=&quot;169&quot; data-ke-size=&quot;size16&quot;&gt;이 알고리즘은 &lt;b&gt;모든 요청의 발생 시각을 로그 형태로 기록&lt;/b&gt;합니다. 이후 새로운 요청이 들어올 때마다 &lt;b&gt;현재 시점을 기준으로 최근 일정 시간(예: 최근 1분)&lt;/b&gt; 동안의 요청 개수를 계산하여 처리 여부를 결정합니다.&lt;/p&gt;
&lt;p data-end=&quot;393&quot; data-start=&quot;297&quot; data-ke-size=&quot;size16&quot;&gt;따라서 특정 시점에 트래픽이 집중되더라도 고정된 시간 구간에 영향을 받지 않으며, 실제 트래픽 흐름을 기준으로 Rate Limiting을 수행할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9Hrif/dJMcafmRCfA/JGaDrpl5raPZVva4e0u9bK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9Hrif/dJMcafmRCfA/JGaDrpl5raPZVva4e0u9bK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9Hrif/dJMcafmRCfA/JGaDrpl5raPZVva4e0u9bK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9Hrif%2FdJMcafmRCfA%2FJGaDrpl5raPZVva4e0u9bK%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;776&quot; height=&quot;195&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 모든 요청의 로그를 저장해야 하기 때문에 메모리 사용량이 증가한다는 단점이 있습니다. 또한 새로운 요청이 들어올 때마다 &lt;b&gt;현재 시점을 기준으로 오래된 로그를 제거하고, 남아 있는 요청 수를 계산&lt;/b&gt;해야 하므로 처리 비용이 증가할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;268&quot; data-start=&quot;187&quot; data-ke-size=&quot;size16&quot;&gt;즉, 이동 윈도 로깅은 정확도가 높은 대신, 많은 요청이 발생하는 환경에서는 메모리 사용량과 연산 비용이 커질 수 있다는 단점을 가지고 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;268&quot; data-start=&quot;187&quot; data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 나온것이 이동 윈도 카운터입니다.&lt;/p&gt;
&lt;h4 data-end=&quot;268&quot; data-start=&quot;187&quot; data-ke-size=&quot;size20&quot;&gt;이동 윈도 카운터&lt;/h4&gt;
&lt;p data-end=&quot;268&quot; data-start=&quot;187&quot; data-ke-size=&quot;size16&quot;&gt;이동 윈도 로깅의 가장 큰 단점은 &lt;b&gt;모든 요청의 로그를 저장해야 한다는 점&lt;/b&gt;입니다. 요청이 많아질수록 메모리 사용량이 증가하고, 요청을 계산하는 비용도 함께 커질 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;332&quot; data-start=&quot;181&quot; data-ke-size=&quot;size16&quot;&gt;이러한 문제를 개선하기 위해 등장한 것이 &lt;b&gt;이동 윈도 카운터(Sliding Window Counter)&lt;/b&gt; 입니다. 이동 윈도 카운터는 모든 요청을 저장하지 않고, &lt;b&gt;이전 윈도우의 요청 수와 현재 윈도우의 요청 수만을 이용하여 현재 시점의 요청 수를 계산합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;490&quot; data-start=&quot;337&quot; data-ke-size=&quot;size16&quot;&gt;즉, 모든 로그를 관리하는 대신 &lt;b&gt;두 개의 윈도우 정보를 활용해 현재 요청량을 추정&lt;/b&gt;하는 방식입니다. 이로 인해 이동 윈도 로깅보다 메모리 사용량이 적고 연산 비용도 낮아지지만, 실제 요청 수를 계산하는 것이 아니라 추정하는 방식이므로 약간의 오차가 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmQ0GP/dJMcahybimP/dNgtoajdZMuwU08EgqpGfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmQ0GP/dJMcahybimP/dNgtoajdZMuwU08EgqpGfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmQ0GP/dJMcahybimP/dNgtoajdZMuwU08EgqpGfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmQ0GP%2FdJMcahybimP%2FdNgtoajdZMuwU08EgqpGfk%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;554&quot; height=&quot;411&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산식은 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1782576880788&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;예상 요청 수 = 이전 윈도우 요청 수 &amp;times; (남은 시간 비율) + 현재 윈도우 요청 수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 이러한 계산식을 사용하는 것일까요?&lt;/p&gt;
&lt;p data-end=&quot;603&quot; data-start=&quot;451&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 어째서 이러한 계산식을 사용하는 것일까요? 이전 윈도우의 요청 수는 이미 집계가 완료된 값이지만, 현재 윈도우는 아직 진행 중이기 때문에 최종 요청 수를 알 수 없습니다.&lt;/p&gt;
&lt;p data-end=&quot;570&quot; data-start=&quot;416&quot; data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;현재 윈도우가 얼마나 진행되었는지를 시간의 비율로 계산하여, 이전 윈도우에서 현재 시점까지 영향을 미치는 요청 수를 추정합니다.&lt;/b&gt; 이후 여기에 현재 윈도우에서 발생한 요청 수를 더하면, 현재 시점을 기준으로 최근 1분 동안의 요청량을 근사치로 계산할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;570&quot; data-start=&quot;416&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 이전 윈도우의 요청 수가 &lt;b&gt;80개&lt;/b&gt;, 현재 윈도우의 요청 수가 &lt;b&gt;30개&lt;/b&gt;라고 가정해보겠습니다. 또한 윈도우는 1분 단위로 갱신되며, 현재 시각은 새로운 윈도우가 시작된 지 &lt;b&gt;20초가 지난 시점&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;1072&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGbR7b/dJMcadP0lCd/kUjaJmahLvJKjsVef7ofd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGbR7b/dJMcadP0lCd/kUjaJmahLvJKjsVef7ofd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGbR7b/dJMcadP0lCd/kUjaJmahLvJKjsVef7ofd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGbR7b%2FdJMcadP0lCd%2FkUjaJmahLvJKjsVef7ofd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;523&quot; height=&quot;228&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 현재 시점을 기준으로 최근 1분인 &lt;b&gt;00:20 ~ 01:20&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;1030&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIYL8i/dJMcag64K6W/JOr8corp4NgUGhI1ojkSG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIYL8i/dJMcag64K6W/JOr8corp4NgUGhI1ojkSG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIYL8i/dJMcag64K6W/JOr8corp4NgUGhI1ojkSG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIYL8i%2FdJMcag64K6W%2FJOr8corp4NgUGhI1ojkSG1%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;554&quot; height=&quot;283&quot; data-origin-width=&quot;1030&quot; data-origin-height=&quot;526&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-end=&quot;544&quot; data-start=&quot;429&quot; data-ke-size=&quot;size16&quot;&gt;현재 윈도우가 20초 진행되었다는 것은, 최근 1분에는 &lt;b&gt;이전 윈도우의 마지막 40초&lt;/b&gt;만 포함된다는 의미입니다. 따라서 이전 윈도우의 요청 수 80개 중 &lt;b&gt;2/3&lt;/b&gt;만 반영하여 다음과 같이 계산합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1782577464432&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;80 &amp;times; 2/3 ≒ 53&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;604&quot; data-start=&quot;569&quot; data-ke-size=&quot;size16&quot;&gt;여기에 현재 윈도우에서 발생한 요청 수 &lt;b&gt;30개&lt;/b&gt;를 더하면,&lt;/p&gt;
&lt;pre id=&quot;code_1782577472031&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;53 + 30 = 83&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;684&quot; data-start=&quot;628&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;00:20 ~ 01:20&lt;/b&gt; 구간에서는 약 &lt;b&gt;83개의 요청&lt;/b&gt;이 발생한 것으로 추정할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;684&quot; data-start=&quot;628&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 이동 윈도 로깅 방식에서 문제가 되었던 &lt;b&gt;메모리 사용량&lt;/b&gt;을 크게 줄일 수 있습니다. 하지만 한편으로는 모든 요청을 저장하지 않고 &lt;b&gt;추정값&lt;/b&gt;을 이용한다는 점에서, 정확도가 떨어지는 것은 아닐까 하는 의문이 생길 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;222&quot; data-start=&quot;179&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 과연 이러한 근사 계산만으로도 트래픽을 안전하게 제어할 수 있을까요?&lt;/p&gt;
&lt;p data-end=&quot;222&quot; data-start=&quot;179&quot; data-ke-size=&quot;size16&quot;&gt;이러한 의문에 대한 대표적인 사례로 Cloudflare의 실험을 들 수 있습니다. Cloudflare는 이동 윈도 카운터(Sliding Window Counter) 와 이동 윈도 로깅(Sliding Window Log) 을 비교하기 위해 약 4억 건의 요청을 대상으로 실험을 진행했습니다.&lt;/p&gt;
&lt;p data-end=&quot;401&quot; data-start=&quot;221&quot; data-ke-size=&quot;size16&quot;&gt;그 결과, 오분류율(Misclassification Rate)은 0.003%에 불과했으며, 평균 추정 오차는 약 6% 수준으로 나타났습니다. 또한 임계치 이하의 요청이 잘못 차단된 사례는 없었고, 임계치를 초과했음에도 허용된 요청 역시 대부분 임계치 근처에서만 발생한 것으로 확인되었습니다.&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;406&quot; data-ke-size=&quot;size16&quot;&gt;즉, 이동 윈도 카운터는 모든 요청을 저장하지 않더라도, 실제 서비스에서는 이동 윈도 로깅과 매우 유사한 수준의 정확도를 제공하면서 메모리 사용량과 연산 비용을 효과적으로 줄일 수 있는 알고리즘이라는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1782578445239&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;Rate Limiting Strategies: Token Bucket, Leaky Bucket, and Sliding Window &amp;mdash; Sujeet Jaiswal - Principal Software Engineer&quot; data-og-description=&quot;Five rate limiting algorithms compared &amp;mdash; token bucket, leaky bucket, fixed window, sliding window log, and sliding window counter &amp;mdash; with how AWS API Gateway, Stripe, Cloudflare, GitHub, and NGINX deploy them, plus distributed coordination patterns usin&quot; data-og-host=&quot;sujeet.pro&quot; data-og-source-url=&quot;https://sujeet.pro/articles/rate-limiting-strategies?utm_source=chatgpt.com&quot; data-og-url=&quot;https://sujeet.pro/articles/rate-limiting-strategies&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://sujeet.pro/articles/rate-limiting-strategies?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://sujeet.pro/articles/rate-limiting-strategies?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;Rate Limiting Strategies: Token Bucket, Leaky Bucket, and Sliding Window &amp;mdash; Sujeet Jaiswal - Principal Software Engineer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Five rate limiting algorithms compared &amp;mdash; token bucket, leaky bucket, fixed window, sliding window log, and sliding window counter &amp;mdash; with how AWS API Gateway, Stripe, Cloudflare, GitHub, and NGINX deploy them, plus distributed coordination patterns usin&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;sujeet.pro&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-end=&quot;333&quot; data-start=&quot;189&quot; data-ke-size=&quot;size16&quot;&gt;하지만 그렇다고 해서 이동 윈도 카운터를 모든 상황에서 만능 해결책처럼 사용할 수 있는 것은 아닙니다. 이동 윈도 카운터는 이전 윈도우와 현재 윈도우를 기반으로 요청 수를 &lt;b&gt;추정&lt;/b&gt;하는 방식이기 때문에, 실제 요청 수와는 약간의 오차가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;547&quot; data-start=&quot;338&quot; data-ke-size=&quot;size16&quot;&gt;일반적인 웹 서비스에서는 이러한 오차가 거의 문제가 되지 않을 수 있습니다. 하지만 대규모 이벤트나 티켓 예매처럼 짧은 시간 동안 트래픽이 폭발적으로 몰리는 환경에서는 작은 오차도 큰 차이를 만들어낼 수 있습니다. 따라서 서비스의 특성과 트래픽 패턴을 고려하여, 필요한 경우에는 이동 윈도 로깅과 같이 더 높은 정확도를 제공하는 알고리즘을 선택하는 것이 적절할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100.814%; height: 186px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style5&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; text-align: center; width: 9.06977%;&quot;&gt;&lt;b&gt;알고리즘&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center; width: 27.786%;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center; width: 35.001%;&quot;&gt;단점&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center; width: 28.8409%;&quot;&gt;추천 사용 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 57px;&quot;&gt;
&lt;td style=&quot;height: 57px; text-align: center; width: 9.06977%;&quot;&gt;&lt;b&gt;고정 윈도 카운터&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 57px; text-align: left; width: 27.786%;&quot;&gt;구현이 단순하고 메모리 사용량이 적다.&lt;/td&gt;
&lt;td style=&quot;height: 57px; text-align: left; width: 35.001%;&quot;&gt;윈도우 경계에서 순간적으로 많은 요청을 허용하는 Boundary Problem이 발생할 수 있다.&lt;/td&gt;
&lt;td style=&quot;height: 57px; text-align: left; width: 28.8409%;&quot;&gt;내부 시스템, 간단한 Rate Limiter, 정확도가 크게 중요하지 않은 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;height: 38px; text-align: center; width: 9.06977%;&quot;&gt;&lt;b&gt;이동 윈도 로깅&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 38px; text-align: left; width: 27.786%;&quot;&gt;최근 요청을 정확하게 계산하여 가장 높은 정확도를 제공한다.&lt;/td&gt;
&lt;td style=&quot;height: 38px; text-align: left; width: 35.001%;&quot;&gt;모든 요청을 저장해야 하므로 메모리 사용량과 연산 비용이 크다.&lt;/td&gt;
&lt;td style=&quot;height: 38px; text-align: left; width: 28.8409%;&quot;&gt;로그인, 결제, 금융, 보안 정책 등 정확도가 중요한 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 57px;&quot;&gt;
&lt;td style=&quot;height: 57px; text-align: center; width: 9.06977%;&quot;&gt;&lt;b&gt;이동 윈도 카운터&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 57px; text-align: left; width: 27.786%;&quot;&gt;적은 메모리로 높은 정확도를 제공하며 성능과 정확도의 균형이 좋다.&lt;/td&gt;
&lt;td style=&quot;height: 57px; text-align: left; width: 35.001%;&quot;&gt;근사치를 계산하므로 약간의 오차가 발생할 수 있다.&lt;/td&gt;
&lt;td style=&quot;height: 57px; text-align: left; width: 28.8409%;&quot;&gt;API Gateway, 일반 REST API, 대부분의 웹 서비스, 대규모 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-end=&quot;641&quot; data-start=&quot;479&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Rate Limiter의 대표적인 알고리즘에 대해 학습해보았습니다. 크게 버킷 기반 알고리즘과 윈도우 기반 알고리즘으로 구분할 수 있으며, 각각의 특징과 장단점을 살펴보았습니다.&lt;/p&gt;
&lt;p data-end=&quot;418&quot; data-start=&quot;264&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로 버킷 기반 알고리즘은 순간적으로 몰리는 트래픽을 제어하는 1차 방어막이라는 느낌을 받았습니다. 또한 Token Bucket과 Leaky Bucket은 목적이 조금 다르기 때문에, 상황에 따라 함께 사용하더라도 크게 문제되지 않을 수 있다고 생각합니다.&lt;/p&gt;
&lt;p data-end=&quot;584&quot; data-start=&quot;420&quot; data-ke-size=&quot;size16&quot;&gt;반면 윈도우 기반 알고리즘은 일정 시간 동안의 요청을 기준으로 트래픽을 관리하는 방식입니다. 따라서 고정 윈도우 카운터, 이동 윈도 로깅, 이동 윈도 카운터는 각각의 특성을 고려하여 서비스 환경에 맞게 선택하는 것이 더 적합하다고 생각합니다.&lt;/p&gt;
&lt;p data-end=&quot;701&quot; data-start=&quot;586&quot; data-ke-size=&quot;size16&quot;&gt;결국, 어느 알고리즘이 절대적으로 우수하다고 말하기는 어렵습니다. 중요한 것은 서비스의 트래픽 특성과 요구사항을 이해하고, 그에 맞는 Rate Limiting 알고리즘을 선택하는 것이라고 생각합니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/585</guid>
      <comments>https://b-programmer.tistory.com/585#entry585comment</comments>
      <pubDate>Sun, 28 Jun 2026 01:59:19 +0900</pubDate>
    </item>
    <item>
      <title>로그인은 어떤 방식으로 개발해야 안전할까?</title>
      <link>https://b-programmer.tistory.com/584</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;로그인을 처리하는 방식에는 크게 세션 방식과 토큰 방식이 있습니다. 토큰 방식을 사용하는 대표적인 이유는 stateless하기 때문입니다. 여기서 stateless하다는 것은 서버가 로그인 상태를 직접 저장하지 않아도 된다는 뜻입니다. 즉, 사용자가 로그인한 뒤 요청을 보낼 때마다 토큰을 함께 전달하고, 서버는 그 토큰이 유효한지만 검증하면 됩니다. 토큰 방식의 장점은 트래픽이 많아졌을 때 조금 더 잘 드러납니다. 세션 방식은 로그인 상태를 서버나 세션 저장소에 보관해야 하기 때문에, 요청이 많아질수록 세션 조회 비용이 계속 발생합니다. 반면, 토큰 방식은 서버가 별도의 로그인 상태를 저장하지 않아도 되기 때문에, 많은 요청이 동시에 들어와도 상대적으로 서버 부담이 적을 수 있습니다. 물론 실제 서비스에서는 중복 로그인 확인, 로그아웃 처리, refresh token 관리 등을 위해 데이터베이스나 Redis 같은 저장소를 함께 사용하는 경우도 많습니다. 하지만 여기서는 그런 예외 상황은 제외하고, 순수하게 세션 방식과 토큰 방식 중 트래픽이 많을 때 어떤 차이가 발생하는지를 비교해보겠습니다.&lt;/p&gt;
&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;p data-ke-size=&quot;size16&quot;&gt;세션 방식은 사용자가 로그인에 성공하면 서버가 세션을 생성하고 저장하는 방식입니다. 이후 서버는 사용자를 식별할 수 있는 고유한 세션 ID를 발급하며, 클라이언트는 이 값을 쿠키에 저장하게 됩니다. 사용자가 요청을 보낼 때마다 쿠키에 저장된 세션 ID가 함께 전달되고, 서버는 해당 세션 ID를 이용해 저장된 세션 정보를 조회한 뒤 인증 여부를 확인합니다. &lt;br /&gt;&lt;br /&gt;즉, 실제 로그인 정보는 서버가 관리하고 있으며, 클라이언트는 단순히 세션을 식별하기 위한 키(Session ID)만 가지고 있는 구조입니다.&lt;br /&gt;간단히 표현하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmOWcg/dJMcab5B0Mv/plWikVuI1FcNdFK7ypXyS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmOWcg/dJMcab5B0Mv/plWikVuI1FcNdFK7ypXyS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmOWcg/dJMcab5B0Mv/plWikVuI1FcNdFK7ypXyS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmOWcg%2FdJMcab5B0Mv%2FplWikVuI1FcNdFK7ypXyS0%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;366&quot; height=&quot;244&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;301&quot; data-start=&quot;283&quot; data-ke-size=&quot;size16&quot;&gt;세션 방식의 특징은 서버가 사용자의 로그인 상태를 직접 관리하기 때문에 강제 로그아웃이나 중복 로그인 제어가 비교적 쉽다는 점입니다. 반면 사용자가 많아질수록 서버 또는 세션 저장소가 관리해야 하는 데이터도 함께 증가하게 됩니다.&lt;/p&gt;
&lt;h3 data-end=&quot;301&quot; data-start=&quot;283&quot; data-ke-size=&quot;size23&quot;&gt;토큰 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 방식은 사용자가 로그인에 성공하면 서버가 토큰을 발급하고, 이후 사용자는 요청마다 해당 토큰을 함께 전달하는 방식입니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;서버는 별도의 로그인 상태를 저장하지 않고 토큰의 유효성만 검증하기 때문에 &lt;/span&gt;&lt;b&gt;Stateless&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;한 구조를 만들 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AMsFX/dJMcahdH6Av/CY8N2S2FKWIYioHj4ER9JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AMsFX/dJMcahdH6Av/CY8N2S2FKWIYioHj4ER9JK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AMsFX/dJMcahdH6Av/CY8N2S2FKWIYioHj4ER9JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAMsFX%2FdJMcahdH6Av%2FCY8N2S2FKWIYioHj4ER9JK%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;366&quot; height=&quot;244&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&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;/h2&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;433&quot; data-start=&quot;293&quot; data-ke-size=&quot;size16&quot;&gt;세션 방식과 토큰 방식의 공통점은 모두 인증을 위해 사용되는 기술이라는 점입니다. 따라서 회원가입과 로그인을 제공하는 대부분의 서비스에서 사용됩니다.&lt;/p&gt;
&lt;p data-end=&quot;229&quot; data-start=&quot;196&quot; data-ke-size=&quot;size16&quot;&gt;하지만 모든 기술이 하나의 목적에만 사용되는 것은 아닙니다.&lt;/p&gt;
&lt;p data-end=&quot;381&quot; data-start=&quot;234&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 세션 방식은 반드시 로그인에만 사용되는 것이 아닙니다. 대표적으로 장바구니 기능이 있습니다. 사용자가 로그인하지 않았더라도 세션을 이용해 장바구니 정보를 저장할 수 있습니다. 이 경우 세션은 인증이 아닌 사용자의 상태를 유지하기 위한 용도로 활용됩니다.&lt;/p&gt;
&lt;p data-end=&quot;519&quot; data-start=&quot;386&quot; data-ke-size=&quot;size16&quot;&gt;토큰 방식 역시 로그인에만 사용되는 것은 아닙니다. 외부 API를 호출할 때 사용하는 API Key나 Access Token도 넓은 의미에서는 토큰 기반 인증 방식에 해당합니다. 사용자가 아닌 시스템 간 통신에서도 토큰은 자주 활용됩니다.&lt;/p&gt;
&lt;p data-end=&quot;661&quot; data-start=&quot;524&quot; data-ke-size=&quot;size16&quot;&gt;즉, 세션과 토큰은 주로 인증에 사용되지만, 본질적으로는 사용자를 식별하거나 특정 권한을 증명하기 위한 수단이라고 볼 수 있습니다. 따라서 로그인뿐만 아니라 상태 유지, 시스템 간 인증, API 접근 제어 등 다양한 영역에서 활용될 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;661&quot; data-start=&quot;524&quot; data-ke-size=&quot;size16&quot;&gt;하지만 저는 이 모든것을 구현하지 않을겁니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3jh6K/dJMcabq1Ilg/9DDHHPqvKX0CKtnIwdG7fK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3jh6K/dJMcabq1Ilg/9DDHHPqvKX0CKtnIwdG7fK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3jh6K/dJMcabq1Ilg/9DDHHPqvKX0CKtnIwdG7fK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3jh6K%2FdJMcabq1Ilg%2F9DDHHPqvKX0CKtnIwdG7fK%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;664&quot; height=&quot;197&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계획은 단순합니다. 사용자가 API를 호출하면 세션 또는 토큰을 통해 인증을 수행하고, 인증에 성공한 경우 &quot;hello&quot; 문자열을 반환하도록 구현할 예정입니다.&lt;/p&gt;
&lt;p data-end=&quot;196&quot; data-start=&quot;123&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;1300&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wGSjs/dJMcajigWpR/l6r5RILEkTMrIBPEqqzfC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wGSjs/dJMcajigWpR/l6r5RILEkTMrIBPEqqzfC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wGSjs/dJMcajigWpR/l6r5RILEkTMrIBPEqqzfC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwGSjs%2FdJMcajigWpR%2Fl6r5RILEkTMrIBPEqqzfC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;611&quot; height=&quot;421&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-is-intersecting=&quot;true&quot; data-turn-id-container=&quot;request-WEB:398869f2-33aa-48be-8dee-ec5ab347d207-14&quot;&gt;
&lt;div&gt;
&lt;div data-conversation-screenshot-content=&quot;&quot;&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5&quot; data-message-id=&quot;9f63b0e3-9356-4c30-9f11-f7bca1188155&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-end=&quot;110&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;예상대로라면 토큰 방식이 더 빠를 것이라 생각했습니다. 서버가 상태를 저장하지 않는 Stateless 구조이기 때문에, 세션 조회 비용이 존재하는 세션 방식보다 유리할 것이라고 예상했기 때문입니다. 하지만 실제 결과를 확인해보니 오히려 세션 방식이 더 빠르게 측정되었습니다. 특히 30K 트래픽 구간에서는 세션 방식이 토큰 방식보다 약 2배 이상 빠른 결과를 보여주었습니다.&lt;/p&gt;
&lt;p data-end=&quot;235&quot; data-start=&quot;212&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 이런 결과가 발생한 것일까요?&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 인증 과정에서 수행되는 작업의 차이에 있습니다. 세션 방식은 클라이언트가 전달한 &lt;u&gt;Session ID를 기반으로 서버에 저장된 세션 정보를 조회하면 인증이 완료&lt;/u&gt;됩니다. 반면 토큰 방식은 요청이 들어올 때마다 토큰의 서명을 검증하고, 만료 시간을 확인하며, 토큰의 무결성을 검증하는 과정을 수행해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;547&quot; data-start=&quot;414&quot; data-ke-size=&quot;size16&quot;&gt;특히 JWT를 사용한다면 Base64 디코딩, Signature 검증(HMAC, RSA, ECDSA 등), Claims 파싱 과정이 매 요청마다 발생하게 됩니다. 이 과정은 서버 메모리 조회보다 CPU 사용량이 더 높은 작업에 해당합니다.&lt;/p&gt;
&lt;p data-end=&quot;685&quot; data-start=&quot;549&quot; data-ke-size=&quot;size16&quot;&gt;즉, 흔히 토큰 방식이 더 빠를 것이라고 생각하기 쉽지만, 실제로는 세션 조회 비용보다 토큰 검증 비용이 더 클 수 있습니다. 따라서 단순히 인증 처리 성능만 비교한다면 세션 방식이 더 빠르게 측정되는 경우도 충분히 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;850&quot; data-start=&quot;687&quot; data-ke-size=&quot;size16&quot;&gt;결국 토큰 방식의 장점은 &lt;u&gt;인증 속도 자체가 아니라 Stateless 구조를 통한 확장성&lt;/u&gt;에 있습니다. 서버가 로그인 상태를 저장하지 않아도 되기 때문에 여러 대의 서버를 운영하거나 MSA 환경으로 확장할 때 관리가 쉬워지는 것이지, 반드시 요청 처리 속도가 더 빠르다는 의미는 아닙니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;850&quot; data-start=&quot;687&quot; data-ke-size=&quot;size16&quot;&gt;여기서 알수 있는 사실은 단순히 속도가 빨라서 토큰을 사용하는게 아니라는 뜻입니다.&lt;/p&gt;
&lt;h2 data-end=&quot;850&quot; data-start=&quot;687&quot; data-ke-size=&quot;size26&quot;&gt;그렇다면, 실행&amp;nbsp; 환경이 늘어나면 어떻게 될까요?&lt;br /&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;토큰 방식의 장점은 stateless하다는 점입니다. 그렇다면, 이 점을 가장 극대화를 시키려면 어떻게 해야 할까요?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UmER3/dJMcabLotq6/i6v6WZdiYK3I5N0eQMhKX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UmER3/dJMcabLotq6/i6v6WZdiYK3I5N0eQMhKX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UmER3/dJMcabLotq6/i6v6WZdiYK3I5N0eQMhKX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUmER3%2FdJMcabLotq6%2Fi6v6WZdiYK3I5N0eQMhKX0%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;344&quot; height=&quot;305&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 실행 환경(Pod 또는 WAS)이 여러 개라고 가정해봅시다. 세션 방식은 기본적으로 서버가 로그인 상태를 저장합니다. 따라서 각 서버가 자신만의 세션 저장소를 가지고 있다면 문제가 발생할 수 있습니다. 예를 들어, 사용자가 실행 환경 1에서 로그인하여 세션이 생성되었다고 가정해보겠습니다. 이후 다음 요청이 로드밸런서에 의해 실행 환경 2로 전달된다면, 실행 환경 2는 해당 세션 정보를 가지고 있지 않기 때문에 사용자를 인증할 수 없습니다.&lt;/p&gt;
&lt;p data-end=&quot;410&quot; data-start=&quot;366&quot; data-ke-size=&quot;size16&quot;&gt;즉, 사용자는 이미 로그인했음에도 불구하고 다시 로그인을 요구받을 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;515&quot; data-start=&quot;415&quot; data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 &lt;u&gt;Sticky Session&lt;/u&gt;을 사용하거나,&lt;u&gt; Redis와 같은 외부 세션 저장소를 두어 모든 실행 환경이 동일한 세션 정보를 조회할 수 있도록 구성&lt;/u&gt;합니다.&lt;/p&gt;
&lt;p data-end=&quot;515&quot; data-start=&quot;415&quot; data-ke-size=&quot;size16&quot;&gt;반면, 토큰 방식은 서버가 로그인 상태를 저장하지 않습니다. 사용자가 토큰만 가지고 있다면 어떤 실행 환경으로 요청이 전달되더라도 동일한 방식으로 인증을 수행할 수 있습니다. 이러한 특징 때문에 분산 환경에서는 토큰 방식이 확장성 측면에서 유리하다고 평가받습니다.&lt;/p&gt;
&lt;p data-end=&quot;515&quot; data-start=&quot;415&quot; data-ke-size=&quot;size16&quot;&gt;하지만 현재 테스트 방식은 실제 로그인 방식과는 다릅니다. 사용자가 로그인하고 인증 정보를 발급받는 구조가 아니라, 단순히 API를 요청하면 인증 과정을 거친 뒤 &quot;hello&quot; 문자열을 반환하는 단순한 코드입니다. 따라서 세션 방식과 토큰 방식을 더 정확하게 비교하려면, 실제 로그인 과정까지 포함하도록 코드를 변경할 필요가 있다고 생각합니다. 그렇다고 해서 위 테스트가 의미 없다는 뜻은 아닙니다. 단일 실행 환경에서는 세션 방식과 토큰 방식 모두 큰 구조적 차이 없이 동작하기 때문에, 결과적으로는 지연 시간 차이만 발생하고 전체적인 흐름은 비슷하게 나올 것이라고 예상할 수 있습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;515&quot; data-start=&quot;415&quot; data-ke-size=&quot;size26&quot;&gt;로그인 방식으로 변경해봅시다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 환경을 3개로 늘려서 테스트를 진행해보겠습니다. 그렇다고 해서&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;1362&quot; data-origin-height=&quot;1226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MG5NC/dJMcafGZA3b/o8hLNklctlJ4bLk7ScKqOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MG5NC/dJMcafGZA3b/o8hLNklctlJ4bLk7ScKqOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MG5NC/dJMcafGZA3b/o8hLNklctlJ4bLk7ScKqOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMG5NC%2FdJMcafGZA3b%2Fo8hLNklctlJ4bLk7ScKqOK%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;438&quot; height=&quot;394&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;1226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;515&quot; data-start=&quot;415&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 테스트하면 곤란해집니다. 왜냐하면 실행 환경이 여러 개인 것처럼 보이더라도, 실제로는 실행 환경이 한 개인 것과 큰 차이가 없기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;515&quot; data-start=&quot;415&quot; data-ke-size=&quot;size16&quot;&gt;그래서 저는 중간에 Nginx를 두고 테스트를 진행할 예정입니다. 이렇게 하면 사용자는 하나의 주소로만 접근하지만, Nginx가 요청을 여러 서버로 분산시켜 실제 운영 환경과 유사한 구조를 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rXZGu/dJMcaaZV9I0/cqaCKhGcj9b4pc6XSgs3v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rXZGu/dJMcaaZV9I0/cqaCKhGcj9b4pc6XSgs3v0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rXZGu/dJMcaaZV9I0/cqaCKhGcj9b4pc6XSgs3v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrXZGu%2FdJMcaaZV9I0%2FcqaCKhGcj9b4pc6XSgs3v0%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;287&quot; height=&quot;385&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;515&quot; data-start=&quot;415&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;1290&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WmWmE/dJMcadI21Rh/KDEnO4TjYjaKqcsc0k4FVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WmWmE/dJMcadI21Rh/KDEnO4TjYjaKqcsc0k4FVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WmWmE/dJMcadI21Rh/KDEnO4TjYjaKqcsc0k4FVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWmWmE%2FdJMcadI21Rh%2FKDEnO4TjYjaKqcsc0k4FVk%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;655&quot; height=&quot;387&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지연시간을 확인해보니 생각보다 흥미로운 결과가 나왔습니다. 1,000건까지는 토큰 방식의 지연시간이 세션 방식보다 조금 더 높게 나타났습니다. 하지만 5,000건부터 10,000건 구간에서는 오히려 토큰 방식의 지연시간이 더 낮게 측정되었습니다.&lt;/p&gt;
&lt;p data-end=&quot;244&quot; data-start=&quot;177&quot; data-ke-size=&quot;size16&quot;&gt;그런데 트래픽이 20,000건을 넘어가자 다시 토큰 방식의 지연시간이 증가하면서 세션 방식보다 느린 결과가 나타났습니다.&lt;/p&gt;
&lt;p data-end=&quot;399&quot; data-start=&quot;249&quot; data-ke-size=&quot;size16&quot;&gt;일반적으로 토큰 방식은 매 요청마다 토큰을 검증해야 하므로 세션 방식보다 불리할 것이라고 예상했습니다. 하지만 실제 결과는 단순하지 않았습니다. 특정 구간에서는 토큰 방식이 더 좋은 성능을 보여주었고, 트래픽이 더욱 증가하자 다시 역전되는 모습을 확인할 수 있었습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이는 단순히 &quot;세션이 빠르다&quot;, &quot;토큰이 빠르다&quot;로 결론 내릴 수 있는 문제가 아니라, 트래픽 규모와 실행 환경에 따라 결과가 달라질 수 있다는 점을 보여주는 사례라고 생각합니다.&lt;/span&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;574&quot; data-origin-height=&quot;356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wyEvW/dJMcagTpLai/QgfLi3nzRGKy8KUYtZY8X0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wyEvW/dJMcagTpLai/QgfLi3nzRGKy8KUYtZY8X0/img.png&quot; data-alt=&quot;100 - 인증 성공률 = 커넥션 에러&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wyEvW/dJMcagTpLai/QgfLi3nzRGKy8KUYtZY8X0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwyEvW%2FdJMcagTpLai%2FQgfLi3nzRGKy8KUYtZY8X0%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;480&quot; height=&quot;298&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;356&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;100 - 인증 성공률 = 커넥션 에러&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 성공률을 확인해보니 더욱 흥미로운 결과가 나왔습니다. 트래픽이 10건에 불과한 상황에서도 세션 방식의 인증 성공률은 16.8%로 매우 낮게 측정되었습니다. 반면 토큰 방식은 85%가 넘는 인증 성공률을 보여주었습니다.&lt;/p&gt;
&lt;p data-end=&quot;309&quot; data-start=&quot;251&quot; data-ke-size=&quot;size16&quot;&gt;이러한 차이가 발생한 이유는 인증 방식 자체의 문제가 아니라 분산 환경에서의 동작 방식 차이 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;423&quot; data-start=&quot;314&quot; data-ke-size=&quot;size16&quot;&gt;세션 방식은 로그인 정보가 각 서버 내부에 저장됩니다. 따라서 사용자가 서버 A에서 로그인하더라도 이후 요청이 서버 B로 전달되면 서버 B는 해당 세션 정보를 알 수 없어 인증에 실패하게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;575&quot; data-start=&quot;428&quot; data-ke-size=&quot;size16&quot;&gt;반면 토큰 방식은 서버가 로그인 상태를 저장하지 않습니다. 사용자가 토큰만 가지고 있다면 어떤 서버로 요청이 전달되더라도 동일한 방식으로 인증을 수행할 수 있습니다. 따라서 여러 서버로 요청이 분산되는 환경에서도 비교적 안정적인 인증 성공률을 보여줄 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;712&quot; data-start=&quot;580&quot; data-ke-size=&quot;size16&quot;&gt;결국 현재 테스트 환경에서 세션 방식의 인증 성공률을 높이기 위해서는 Sticky Session을 적용하거나, Redis와 같은 외부 세션 저장소를 구축하여 모든 서버가 동일한 세션 정보를 조회할 수 있도록 구성해야 할 것으로 보입니다.&lt;/p&gt;
&lt;p data-end=&quot;712&quot; data-start=&quot;580&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;1260&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DsWNz/dJMcaccn8hZ/inqqM7uTktzNFXLKYBUY5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DsWNz/dJMcaccn8hZ/inqqM7uTktzNFXLKYBUY5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DsWNz/dJMcaccn8hZ/inqqM7uTktzNFXLKYBUY5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDsWNz%2FdJMcaccn8hZ%2FinqqM7uTktzNFXLKYBUY5K%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;657&quot; height=&quot;405&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;776&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 환경에서는 세션이나 토큰이나 결과가 크게 다르지 않다는 것을 알 수 있었습니다.&lt;br /&gt;다시 3파드로 넘어와서 위에서 100 - 인증 성공률은 커넥션 실패라고 하였습니다. 과연 그럴까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1946&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZgqUQ/dJMcaip8Cwl/OsQJeXBmNa9hm8LXh3Go21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZgqUQ/dJMcaip8Cwl/OsQJeXBmNa9hm8LXh3Go21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZgqUQ/dJMcaip8Cwl/OsQJeXBmNa9hm8LXh3Go21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZgqUQ%2FdJMcaip8Cwl%2FOsQJeXBmNa9hm8LXh3Go21%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;510&quot; height=&quot;245&quot; data-origin-width=&quot;1946&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 결과를 보니 토큰은 항상 100%를 기록하는거에 비해 세션 파트는 100%가 된적이 단 한번도 존재하지 않았습니다. 이런 현상이 발생한 이유가 무엇 일까요?&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;처음에는 토큰 방식이 Stateless 구조이기 때문에 세션 방식보다 처리 속도가 빠를 것이라고 생각했습니다. 하지만 실제 테스트 결과를 확인해보니 단일 환경에서는 오히려 세션 방식이 토큰 방식보다 더 빠르게 동작한다는 사실을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;250&quot; data-start=&quot;209&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 실행 환경(Pod)이 여러 개로 늘어나면 어떤 현상이 발생할까요?&lt;/p&gt;
&lt;p data-end=&quot;353&quot; data-start=&quot;255&quot; data-ke-size=&quot;size16&quot;&gt;지연시간 측면에서는 단일 환경과 비교했을 때 눈에 띄는 차이를 발견하지 못했습니다. 하지만 인증 성공률을 기준으로 비교해보니 토큰 방식이 압도적으로 높은 결과를 보여주었습니다.&lt;/p&gt;
&lt;p data-end=&quot;575&quot; data-start=&quot;358&quot; data-ke-size=&quot;size16&quot;&gt;특히 세션 방식은 인증 성공률과 HTTP 연결 실패율을 합쳐도 100%가 되지 않는 현상을 발견하였습니다. 처음에는 원인을 이해하지 못했지만, 확인해보니 현재 테스트 환경의 세션 방식은 세션 정보를 서버 간에 공유하지 않고 있었습니다. 즉, 사용자가 로그인한 서버와 요청을 처리하는 서버가 달라질 경우 인증에 실패할 수 있었고, 이러한 구조 때문에 예상치 못한 결과가 발생한 것이었습니다.&lt;/p&gt;
&lt;p data-end=&quot;764&quot; data-start=&quot;580&quot; data-ke-size=&quot;size16&quot;&gt;저는 세션 방식이 구현해야 할 코드가 적기 때문에 토큰 방식보다 단순하다고 생각했습니다. 하지만 실행 환경이 여러 개로 늘어나는 순간 이야기가 달라졌습니다. 세션을 사용하기 위해서는 Sticky Session을 적용하거나 Redis와 같은 공통 세션 저장소를 구축해야 하며, 이 과정에서 추가적인 운영 및 관리 비용이 발생합니다.&lt;/p&gt;
&lt;p data-end=&quot;911&quot; data-start=&quot;769&quot; data-ke-size=&quot;size16&quot;&gt;반면 토큰 방식은 별도의 세션 공유 구조 없이도 여러 실행 환경에서 동일하게 인증을 수행할 수 있었습니다. 물론, 토큰 검증 과정에서 추가적인 연산 비용이 발생하지만, 분산 환경에서는 이러한 특징이 큰 장점으로 작용할 수 있다는 점을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;1028&quot; data-start=&quot;916&quot; data-ke-size=&quot;size16&quot;&gt;이번 학습을 통해 인증 방식은 단순히 처리 속도만으로 판단할 수 있는 문제가 아니라는 것을 깨달았습니다. 실제 서비스에서는 성능뿐만 아니라 확장성, 운영 편의성, 인증 성공률까지 함께 고려해야 합니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;1236&quot; data-start=&quot;1033&quot; data-ke-size=&quot;size16&quot;&gt;또한 토큰 방식에서 중복 로그인 관리, 로그아웃 처리, Refresh Token 관리 등을 위해 Redis나 데이터베이스를 사용하는 것이 Stateless라는 개념과 상충되는 것이 아니라는 점도 배울 수 있었습니다. 중요한 것은 데이터를 저장하느냐가 아니라, 인증 자체를 서버 세션 상태에 의존하지 않는다는 점이라는 사실을 이번 실습을 통해 확인할 수 있었습니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/584</guid>
      <comments>https://b-programmer.tistory.com/584#entry584comment</comments>
      <pubDate>Sun, 14 Jun 2026 22:00:35 +0900</pubDate>
    </item>
    <item>
      <title>한국어 형태소 분석 vs BPE</title>
      <link>https://b-programmer.tistory.com/583</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7awAS/dJMcaf7TmT6/R517KInN87YRrbBkuvsVZK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7awAS/dJMcaf7TmT6/R517KInN87YRrbBkuvsVZK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7awAS/dJMcaf7TmT6/R517KInN87YRrbBkuvsVZK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7awAS%2FdJMcaf7TmT6%2FR517KInN87YRrbBkuvsVZK%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;384&quot; height=&quot;216&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국어를 뉴럴 네트워크로 학습시킬 때는 몇 가지 어려움이 존재합니다. 대표적인 예가 조사입니다. 한국어에는 &amp;lsquo;가&amp;rsquo;, &amp;lsquo;는&amp;rsquo;, &amp;lsquo;이&amp;rsquo;와 같은 조사가 존재하며, 이들은 명사와 결합하면서 문장에서의 역할이나 의미를 바꿀 수 있습니다. 예를 들어 같은 명사라도 어떤 조사가 붙느냐에 따라 주어가 되기도 하고, 목적어나 비교 대상처럼 해석될 수도 있습니다. 따라서 단순히 공백 기준으로 문장을 나누면 단어의 의미나 문법적 역할을 정확하게 파악하기 어렵습니다. 이러한 문제를 해결하기 위해 형태소 분석을 사용할 수 있습니다. 형태소 분석을 통해 명사와 조사를 분리하면, 모델이 단어 자체의 의미와 문장 내 역할을 더 명확하게 학습할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, ChatGPT와 같은 현대적인 LLM은 형태소 분석을 사용하지 않습니다. 대신, BPE(Byte Pair Encoding) 기반의 토크나이징 방식을 사용합니다. 한국어에는 조사와 어미가 존재하기 때문에 과거에는 형태소 분석이 중요하게 여겨졌습니다. 그렇다면 현대의 LLM은 왜 형태소 분석 없이도 한국어를 이해할 수 있는 것일까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;형태소 분석은 왜 해야 하는가?&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 한국어 문장을 신경에 넣으려면 잘게 쪼개야 합니다. 그럼 어떻게 쪼갤 수 있을까요?&lt;br /&gt;방법은 크게 3가지입니다.&lt;br /&gt;&lt;br /&gt;글자 단위로 쪼개는 방법, 공백 단위로 쪼개는 방법, 형태소 단위로 쪼개는 방법이 있습니다.&lt;br /&gt;다음과 같은 문장이 있다고 가정해봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1780741775430&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;오늘 날씨가 화창합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;먼저 글자 단위로 쪼개는 방법에 대해 알아봅시다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 문장을 글자 단위로 쪼개면 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780741813266&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;오,늘,날,씨,가,화,창,합,니,다,&lt;/code&gt;&lt;/pre&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;910&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q1gdk/dJMcaiwO0Cs/jXoBcGHD8SE6pqxkBqGoF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q1gdk/dJMcaiwO0Cs/jXoBcGHD8SE6pqxkBqGoF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q1gdk/dJMcaiwO0Cs/jXoBcGHD8SE6pqxkBqGoF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ1gdk%2FdJMcaiwO0Cs%2FjXoBcGHD8SE6pqxkBqGoF1%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;480&quot; height=&quot;224&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'오'라는 토큰만으로는 다음 글자를 예측하기 어렵습니다. 실제로 '오'로 시작하는 단어는 111개나 존재하기 때문에 어떤 글자가 뒤에 올지 판단할 수 없습니다. 또한 '오'만 보고는 사용자가 어떤 단어를 의도했는지 추론하는 것도 사실상 불가능합니다. 즉, 정보가 너무 부족하여 정확한 예측이 어려워지는 문제가 발생합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그 다음은 공백 단위로 쪼개는 방법입니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방법이 글자 단위로 문장을 분리했다면, 이번에는 공백 단위로 분리하는 방법입니다. 여담으로 현대 LLM에서 사용하는 BPE도 이러한 아이디어에서 출발하여 발전한 방식입니다. (BPE에 대해서는 추후 자세히 설명하겠습니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1780742927062&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;오늘, 날씨가, 화창합니다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼핏 보기에는 큰 문제가 없어 보입니다. 하지만 토큰으로 분리된 단어를 살펴보면 문제가 발생합니다.&lt;br /&gt;'오늘', '화창합니다'는 큰 문제가 없지만, '날씨가'라는 단어는 다릅니다.&lt;/p&gt;
&lt;p data-end=&quot;247&quot; data-start=&quot;110&quot; data-ke-size=&quot;size16&quot;&gt;실제로 우리가 생각하는 단어는 '날씨'이지만, 신경망은 '날씨가', '날씨는', '날씨도'를 모두 서로 다른 단어로 인식합니다. 즉, 하나의 명사에 조사와 어미가 결합하면서 수많은 변형이 생성되고, 이러한 변형들이 모두 별개의 어휘로 취급됩니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;318&quot; data-start=&quot;252&quot; data-ke-size=&quot;size16&quot;&gt;결과적으로 어휘 수가 급격하게 증가하게 되며, 이를 어휘 폭발(Vocabulary Explosion) 문제라고 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이를 해결 하기 위해 형태소 단위로 쪼개게 되었습니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태소 단위로 쪼개면 어떤 장점이 있을까요?&lt;/p&gt;
&lt;pre id=&quot;code_1780743962768&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;오늘,날씨,가,화창,하,ㅂ니다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 분리하면 장점이 '날씨가'와 '날씨는' '날씨를' 등등 의미를 해치지 않고 토큰으로 구별할 수 있는 장점을 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 분리하면 '날씨가', '날씨는', '날씨를'과 같은 단어들을 각각 별개의 단어로 학습할 필요가 없습니다. 공통된 의미를 가진 '날씨'는 그대로 유지하고, 조사만 별도로 구분하여 학습할 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;320&quot; data-start=&quot;247&quot; data-ke-size=&quot;size16&quot;&gt;즉, 의미를 훼손하지 않으면서도 어휘 수를 줄일 수 있으며, 다양한 단어 변형에 보다 효율적으로 대응할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면, 어째서 현대 LLM은 형태소 분석을 하지 않을까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 이유는 언어마다 가진 특성이 너무 다르기 때문입니다. 영어는 공백 단위로 분리해도 큰 문제가 없지만, 한국어는 조사와 어미 때문에 형태소 분석이 필요합니다.&lt;/p&gt;
&lt;p data-end=&quot;333&quot; data-start=&quot;180&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;1586&quot; data-origin-height=&quot;932&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O6aDU/dJMcacQSxmo/vXm66eKB8WKkdimwFaMhv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O6aDU/dJMcacQSxmo/vXm66eKB8WKkdimwFaMhv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O6aDU/dJMcacQSxmo/vXm66eKB8WKkdimwFaMhv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO6aDU%2FdJMcacQSxmo%2FvXm66eKB8WKkdimwFaMhv1%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;572&quot; height=&quot;336&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;932&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;598&quot; data-start=&quot;478&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 다음과 같은 중국어 문장이 있다고 해봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div data-conversation-screenshot-content=&quot;&quot;&gt;
&lt;div data-message-model-slug=&quot;gpt-5-5&quot; data-message-id=&quot;bd937c9b-bfc7-4f1f-b114-f6dd6537247b&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;我喜欢学习人工智能&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;(나는 인공지능 공부를 좋아한다.)&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;328&quot; data-start=&quot;314&quot; data-ke-size=&quot;size16&quot;&gt;중국어는 띄어쓰기가 없기 때문에 다음과 같이 먼저 분절해야 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;我 / 喜欢 / 学习 / 人工智能&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;384&quot; data-start=&quot;374&quot; data-ke-size=&quot;size16&quot;&gt;태국어도 비슷합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ฉันชอบเรียนปัญญาประดิษฐ์ -&amp;gt; ฉัน / ชอบ / เรียน / ปัญญาประดิษฐ์&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;(나는 인공지능 배우는 것을 좋아한다.)&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, BPE가 없던 시절에는 언어마다 전용 전처리기를 만드는 방식이 일반적이었습니다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;523&quot; data-start=&quot;513&quot; data-ke-size=&quot;size16&quot;&gt;문제는 이런 방식이 언어마다 구현해야 해서 유지보수 비용이 컸습니다.&lt;br /&gt;또한 다국어 모델을 만들기가 어려웠으며 신조어 오타 새로운 단어 대응에 약하다는 단점이 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;731&quot; data-start=&quot;611&quot; data-ke-size=&quot;size16&quot;&gt;그래서 현대 LLM은 &quot;&lt;u&gt;언어학자가 규칙을 만들어주는 방식&lt;/u&gt;&quot; 대신 &quot;&lt;u&gt;문자열 패턴을 모델이 스스로 학습하는 방식&lt;/u&gt;&quot;을 선택했고, 그 결과가 BPE, WordPiece, SentencePiece 같은 서브워드 토크나이저입니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그래서 LLM 이전 시대의 NLP는 &lt;/span&gt;언어학(Language-driven)&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 에 가까웠고, 현대 LLM은 &lt;/span&gt;데이터 중심(Data-driven)&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 에 가깝다고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;598&quot; data-start=&quot;478&quot; data-ke-size=&quot;size26&quot;&gt;그렇다면 BPE는 이러한 문제를 어떻게 해결할 수 있을까요?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1Nosz/dJMcac4qagd/7SObTuSePvoadGPMKfyO90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1Nosz/dJMcac4qagd/7SObTuSePvoadGPMKfyO90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1Nosz/dJMcac4qagd/7SObTuSePvoadGPMKfyO90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1Nosz%2FdJMcac4qagd%2F7SObTuSePvoadGPMKfyO90%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;471&quot; height=&quot;314&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;figure id=&quot;og_1780755855900&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;13-01 바이트 페어 인코딩(Byte Pair Encoding, BPE)&quot; data-og-description=&quot;기계에게 아무리 많은 단어를 학습시켜도 세상의 모든 단어를 알려줄 수는 없는 노릇입니다. 만약 기계가 모르는 단어가 등장하면 그 단어를 단어 집합에 없는 단어란 의미에서 해당 토&amp;hellip;&quot; data-og-host=&quot;wikidocs.net&quot; data-og-source-url=&quot;https://wikidocs.net/22592&quot; data-og-url=&quot;https://wikidocs.net/22592&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/q3xwQ/dJMb83SqIVN/hAdmPrNMkNI4sjUsTTI6Nk/img.jpg?width=98&amp;amp;height=130&amp;amp;face=0_0_98_130,https://scrap.kakaocdn.net/dn/rYfR6/dJMb9bv9WZg/pky6ckydAlKNF8m0TnFm20/img.png?width=409&amp;amp;height=353&amp;amp;face=0_0_409_353&quot;&gt;&lt;a href=&quot;https://wikidocs.net/22592&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://wikidocs.net/22592&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/q3xwQ/dJMb83SqIVN/hAdmPrNMkNI4sjUsTTI6Nk/img.jpg?width=98&amp;amp;height=130&amp;amp;face=0_0_98_130,https://scrap.kakaocdn.net/dn/rYfR6/dJMb9bv9WZg/pky6ckydAlKNF8m0TnFm20/img.png?width=409&amp;amp;height=353&amp;amp;face=0_0_409_353');&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;13-01 바이트 페어 인코딩(Byte Pair Encoding, BPE)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;기계에게 아무리 많은 단어를 학습시켜도 세상의 모든 단어를 알려줄 수는 없는 노릇입니다. 만약 기계가 모르는 단어가 등장하면 그 단어를 단어 집합에 없는 단어란 의미에서 해당 토&amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;wikidocs.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태소 분석은 한국어의 조사와 어미를 효과적으로 분리할 수 있지만, 언어마다 다른 분석기가 필요하다는 한계가 있습니다. 또한 신조어나 외래어, 잘 알려지지 않은 언어를 처리하는 데도 어려움이 존재합니다.&lt;/p&gt;
&lt;p data-end=&quot;240&quot; data-start=&quot;179&quot; data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 현대의 LLM은 BPE(Byte Pair Encoding)라는 방식을 사용합니다.&lt;/p&gt;
&lt;p data-end=&quot;265&quot; data-start=&quot;242&quot; data-ke-size=&quot;size16&quot;&gt;BPE의 핵심 아이디어는 매우 단순합니다. &lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;u&gt;&quot;문법은 모르겠고, 자주 같이 등장하는 문자열은 하나로 합치자.&quot;&lt;/u&gt;&lt;br /&gt;&lt;/span&gt;즉, 사전이나 문법 규칙을 사용하지 않고 오직 통계만을 이용하여 토큰을 생성합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;369&quot; data-start=&quot;355&quot; data-section-id=&quot;d82zy5&quot; data-ke-size=&quot;size23&quot;&gt;BPE의 동작 원리&lt;/h3&gt;
&lt;p data-end=&quot;393&quot; data-start=&quot;371&quot; data-ke-size=&quot;size16&quot;&gt;BPE는 다음과 같은 과정을 반복합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;500&quot; data-start=&quot;395&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;417&quot; data-start=&quot;395&quot; data-section-id=&quot;1kuwpgs&quot;&gt;모든 단어를 글자 단위로 분리한다.&lt;/li&gt;
&lt;li data-end=&quot;448&quot; data-start=&quot;418&quot; data-section-id=&quot;11wg7xl&quot;&gt;데이터에서 가장 자주 등장하는 글자 쌍을 찾는다.&lt;/li&gt;
&lt;li data-end=&quot;474&quot; data-start=&quot;449&quot; data-section-id=&quot;x9er0b&quot;&gt;해당 글자 쌍을 하나의 토큰으로 합친다.&lt;/li&gt;
&lt;li data-end=&quot;500&quot; data-start=&quot;475&quot; data-section-id=&quot;eq51uw&quot;&gt;원하는 어휘 크기가 될 때까지 반복한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;533&quot; data-start=&quot;502&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 데이터가 있다고 가정해보겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1780755668983&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;강아지가
강아지를
강아지는&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;602&quot; data-start=&quot;577&quot; data-ke-size=&quot;size16&quot;&gt;초기 상태에서는 모두 글자 단위로 분리됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1780755688295&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;강 아 지 가
강 아 지 를
강 아 지 는&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;698&quot; data-start=&quot;665&quot; data-ke-size=&quot;size16&quot;&gt;이후 가장 자주 등장하는 문자열 쌍을 반복적으로 병합합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1780755743183&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;강아지 + 가
강아지 + 를
강아지 + 는&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;797&quot; data-start=&quot;765&quot; data-ke-size=&quot;size16&quot;&gt;이 과정을 반복하면 다음과 같은 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1780755755332&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;강아지가 &amp;rarr; [강아지, 가]
강아지를 &amp;rarr; [강아지, 를]
강아지는 &amp;rarr; [강아지, 는]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1319&quot; data-start=&quot;1261&quot; data-ke-size=&quot;size16&quot;&gt;흥미로운 점은 형태소 분석기를 사용하지 않았음에도 형태소 분석과 유사한 결과를 얻을 수 있다는 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;1319&quot; data-start=&quot;1261&quot; data-ke-size=&quot;size16&quot;&gt;다만, BPE는 자주 등장하는 문자열을 기준으로 토큰을 생성하기 때문에 항상 의미 있는 패턴을 찾는다고 보장할 수는 없습니다.&lt;/p&gt;
&lt;p data-end=&quot;209&quot; data-start=&quot;106&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이러한 문제는 GPT와 같은 대규모 LLM 환경에서는 크게 발생하지 않습니다. 방대한 데이터가 존재하기 때문에 재사용 가능한 단어와 패턴을 자연스럽게 학습할 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;248&quot; data-start=&quot;214&quot; data-ke-size=&quot;size16&quot;&gt;반대로 데이터셋이 매우 작은 경우에는 문제가 될 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;384&quot; data-start=&quot;253&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 학습 데이터에 &quot;아버지가방에들어가신다&quot; 라는 문장만 반복적으로 존재한다고 가정해보겠습니다. 이 경우 BPE는 해당 문자열을 매우 자주 등장하는 패턴으로 인식하여, 결국 문장 전체를 하나의 토큰으로 학습할 수 있습니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;527&quot; data-start=&quot;389&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 되면 '아버지', '방', '들어가신다' 와 같은 재사용 가능한 패턴을 학습하지 못하게 되고, 새로운 문장에 대한 일반화 성능이 떨어질 수 있습니다. 즉, BPE는 충분히 크고 다양한 데이터셋이 주어질 때 가장 효과적으로 동작합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;527&quot; data-start=&quot;389&quot; data-ke-size=&quot;size23&quot;&gt;이제 본견적으로 BPE로 학습을 진행해보겠습니다.&lt;/h3&gt;
&lt;p data-end=&quot;1319&quot; data-start=&quot;1261&quot; data-ke-size=&quot;size16&quot;&gt;위키를 데이터를 통해 학습을 진행시켜보겠습니다.&lt;br /&gt;테스트 결과 학습은 약 38.2초 만에 완료되었습니다. 이전에 수행했던 형태소 분석 방식이 약 36분이 소요되었던 것과 비교하면 약 56배 빠른 결과입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;1104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tJRjg/dJMcaayPh4q/Er0NIshv7gOT2wfgaG4D0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tJRjg/dJMcaayPh4q/Er0NIshv7gOT2wfgaG4D0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tJRjg/dJMcaayPh4q/Er0NIshv7gOT2wfgaG4D0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtJRjg%2FdJMcaayPh4q%2FEr0NIshv7gOT2wfgaG4D0k%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;560&quot; height=&quot;303&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;1104&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-end=&quot;256&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;이유는 단순합니다. BPE는 형태소 분석처럼 품사나 조사, 어미를 판별하지 않습니다. 단순히 데이터에서 자주 등장하는 문자열 패턴을 찾아 병합하는 과정을 반복할 뿐입니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;320&quot; data-start=&quot;261&quot; data-ke-size=&quot;size16&quot;&gt;즉, 언어학적 규칙을 해석하는 과정이 없기 때문에 형태소 분석보다 훨씬 빠르게 학습을 수행할 수 있습니다.&lt;br /&gt;다만, 그렇다고 해서 BPE가 형태소 분석보다 우수하지는 않습니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;320&quot; data-start=&quot;261&quot; data-ke-size=&quot;size16&quot;&gt;제가 구현한 BPE는 한국어 데이터만을 학습한 상태이기 때문에, 영어 문장을 입력할 경우 토큰 수가 더 많이 증가하는 경향을 보였습니다. 이는 영어 단어가 충분히 병합되지 않아 작은 단위로 분리되기 때문입니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;264&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;물론 GPT와 같은 상용 LLM은 영어를 포함한 다양한 언어를 함께 학습하기 때문에 이러한 문제가 크게 발생하지 않습니다. 따라서 해당 결과는 한국어 데이터만으로 학습한 BPE의 한계로 보는 것이 적절합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2086&quot; data-origin-height=&quot;1052&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSfpqm/dJMcafmBKFG/3G3BKLRLI5bgvR14H9ztV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSfpqm/dJMcafmBKFG/3G3BKLRLI5bgvR14H9ztV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSfpqm/dJMcafmBKFG/3G3BKLRLI5bgvR14H9ztV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSfpqm%2FdJMcafmBKFG%2F3G3BKLRLI5bgvR14H9ztV1%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;561&quot; height=&quot;283&quot; data-origin-width=&quot;2086&quot; data-origin-height=&quot;1052&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어뿐만 아니라 일부 문장에서는 형태소 분석 방식이 BPE보다 더 적은 토큰을 사용하는 경우도 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;206&quot; data-start=&quot;72&quot; data-ke-size=&quot;size16&quot;&gt;사실 이러한 결과는 어떤 데이터를 학습했는지에 따라 크게 달라질 수 있습니다. 특정 단어나 표현이 학습 데이터에 자주 등장했다면 BPE가 더 효율적일 수 있고, 반대로 그렇지 않다면 형태소 분석 방식이 더 적은 토큰을 사용할 수도 있습니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;274&quot; data-start=&quot;211&quot; data-ke-size=&quot;size16&quot;&gt;즉, 토큰 수는 단순히 알고리즘의 우열보다는 학습 데이터의 특성과 분포에 더 큰 영향을 받는다고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;274&quot; data-start=&quot;211&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;1296&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckIMk9/dJMcacQTzM5/TAFcuT88dUM69ZfVo4Akbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckIMk9/dJMcacQTzM5/TAFcuT88dUM69ZfVo4Akbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckIMk9/dJMcacQTzM5/TAFcuT88dUM69ZfVo4Akbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckIMk9%2FdJMcacQTzM5%2FTAFcuT88dUM69ZfVo4Akbk%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;575&quot; height=&quot;426&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험 결과를 종합해보면, 어휘 크기 16K~32K 구간이 가장 균형 잡힌 선택으로 보였습니다.&lt;/p&gt;
&lt;p data-end=&quot;179&quot; data-start=&quot;59&quot; data-ke-size=&quot;size16&quot;&gt;이 구간에서는 토큰 효율이 충분히 높았으며(문장당 약 24~28개), 학습 시간도 약 40초 내외로 빠르게 유지되었습니다. 또한 어휘 수가 지나치게 크지 않아 모델 크기와 메모리 사용량 측면에서도 부담이 적었습니다.&lt;/p&gt;
&lt;p data-end=&quot;313&quot; data-start=&quot;184&quot; data-ke-size=&quot;size16&quot;&gt;반면 64K 이상의 어휘를 사용할 경우 토큰 효율은 일부 개선되지만, 그에 비해 메모리 사용량과 학습 비용이 증가하는 경향을 보였습니다. 한국어만을 대상으로 하는 모델이라면 64K 이상의 어휘는 다소 과한 선택일 수 있습니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;464&quot; data-start=&quot;318&quot; data-ke-size=&quot;size16&quot;&gt;참고로 GPT-2는 약 50K 규모의 어휘를 사용하였으며, GPT-4와 같은 최신 다국어 모델은 100K 이상의 어휘를 사용합니다. 반면, 이번 실험에서 구축한 형태소 기반 어휘는 약 16K 규모로, 한국어를 처리하는 데에는 충분히 실용적인 수준이라고 판단됩니다.&lt;/p&gt;
&lt;h2 data-end=&quot;464&quot; data-start=&quot;318&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 글에서는 형태소 분석을 이용하여 뉴럴 네트워크를 학습시켜보았습니다. 그리고 이번에는 형태소 분석 대신 BPE를 적용하여 동일한 실험을 진행해보았습니다.&lt;/p&gt;
&lt;p data-end=&quot;307&quot; data-start=&quot;130&quot; data-ke-size=&quot;size16&quot;&gt;가장 인상적이었던 부분은 학습 속도였습니다. 형태소 분석 방식은 30분 이상이 소요되었던 반면, BPE는 약 30초 만에 학습을 완료할 수 있었습니다. 물론 토큰 구성 방식과 특성에는 차이가 존재하지만, 언어학적 규칙을 고려하지 않고 순수 통계 기반으로 동작한다는 점에서 상당한 속도상의 이점을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;407&quot; data-start=&quot;312&quot; data-ke-size=&quot;size16&quot;&gt;또한 BPE가 단순히 토큰을 분리하는 기술이 아니라, 현대 LLM이 다양한 언어와 신조어를 처리할 수 있도록 만들어주는 핵심 기술 중 하나라는 점도 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;593&quot; data-start=&quot;412&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는 임베딩 연산에 대해 살펴보겠습니다. 흔히 임베딩은 단순히 단어를 숫자로 변환한 벡터라고 생각하기 쉽습니다. 하지만 실제로는 벡터 간 덧셈과 뺄셈을 통해 의미적인 관계를 표현할 수 있습니다. 다음 글에서는 형태소 분석과 BPE로 생성된 토큰을 바탕으로, 임베딩이 어떻게 의미를 학습하고 연산하는지 알아보겠습니다.&lt;/p&gt;</description>
      <category>개발/LLM</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/583</guid>
      <comments>https://b-programmer.tistory.com/583#entry583comment</comments>
      <pubDate>Mon, 8 Jun 2026 18:30:38 +0900</pubDate>
    </item>
    <item>
      <title>다량의 엑셀 정보는 어떻게 저장할 수 있을까?</title>
      <link>https://b-programmer.tistory.com/582</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 SI 프로젝트를 진행하고 있습니다. &lt;/span&gt;&lt;span&gt;프로젝트의 상세한 내용은 보안상 말씀드릴 수 없지만, 업무를 진행하면서 확인하고 싶은 기술적인 내용을 정리해보려고 합니다. &lt;/span&gt;&lt;span&gt;이번에 다루려는 주제는 &lt;/span&gt;&lt;span&gt;프론트 화면에서 엑셀 파일을 업로드하면, 해당 데이터를 서버에서 읽어 DB에 저장하는 흐름&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;u&gt;여기에 등장하는 코드는 실제 프로젝트 코드가 아니며, 개념 설명을 위해 별도로 작성한 예제 코드&lt;/u&gt;입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;일단, 100만건의 데이터를 엑셀업로드를 진행해봅시다.&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;* 실무에서 정했던 엑셀 데이터 수보다 더 많이 진행할 예정입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;엑셀에는 100만건 이상의 데이터가 존재합니다. 이것을 바로 파일 업로드를 시켜 바로 DB에 넣다보면 서버에서 OOM이 발생할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;테스트 결과 생각보다 100만건의 데이터는 업로드에 성공하였습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rnWC9/dJMcaf7P50j/T1IQVgZwxFQrjN8fykrD3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rnWC9/dJMcaf7P50j/T1IQVgZwxFQrjN8fykrD3K/img.png&quot; data-alt=&quot;4건은 원래 있던 데이터입니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rnWC9/dJMcaf7P50j/T1IQVgZwxFQrjN8fykrD3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrnWC9%2FdJMcaf7P50j%2FT1IQVgZwxFQrjN8fykrD3K%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;663&quot; height=&quot;156&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;4건은 원래 있던 데이터입니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 업로드가 완료가 될때까지 약 25분 정도 흘렀습니다. 또한, 동기 방식으로 데이터를 전송하기 때문에, 모든 데이터가 업로드가 종료가 되어야 DB에 반영이 되는 문제도 있었습니다.&lt;br /&gt;이러한 문제를 해결하기 위해서는 어떻게 해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전송되는 데이터를 쪼개서 처리하면 됩니다. 이를 청크 단위 처리라고 볼 수 있습니다.&lt;br /&gt;예를 들어 100만 건의 데이터를 하나의 큰 그룹으로 보고 한 번에 전송하면, 업로드 요청 한 번에 100만 건이 모두 실리게 됩니다. 하지만 이 데이터를 10개 또는 100개의 그룹으로 나누면, 한 번에 전송되는 데이터 수는 각각 10만 건, 1만 건 수준으로 줄어들게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;296&quot; data-start=&quot;238&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 데이터를 쪼개는 작업은 어느 위치에서 수행할 수 있을까요?&lt;/p&gt;
&lt;p data-end=&quot;296&quot; data-start=&quot;238&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 쪼개는 위치는 크게 세 가지로 볼 수 있습니다. &lt;br /&gt;첫 번째는 프론트 화면에서 엑셀 데이터를 읽고, 일정 단위로 나누어 서버에 전송하는 방식입니다. &lt;br /&gt;두 번째는 프론트에서는 엑셀을 단순한 파일로만 업로드하고, 서버에서 파일을 읽으면서 데이터를 나누어 처리하는 방식입니다. &lt;br /&gt;마지막은 현재 구조와 동일하게 프론트에서 서버로 데이터를 전달한 뒤, DB 적재 단계에서 프로시저를 활용하여 데이터를 나누어 처리하는 방식입니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;356&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;이제 각각의 방식이 어떤 장단점을 가지는지, 그리고 현재 상황에서는 어떤 방식이 가장 적합한지 정리해보겠습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;296&quot; data-start=&quot;238&quot; 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;512&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsREN8/dJMcaalbR0L/nE9CooJqNI96tfpq0C2KP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsREN8/dJMcaalbR0L/nE9CooJqNI96tfpq0C2KP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsREN8/dJMcaalbR0L/nE9CooJqNI96tfpq0C2KP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsREN8%2FdJMcaalbR0L%2FnE9CooJqNI96tfpq0C2KP0%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;284&quot; height=&quot;284&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서 데이터를 쪼개기 위한 전제조건은 브라우저에서 엑셀 파일을 직접 읽어야 한다는 것입니다.&lt;br /&gt;즉, 프론트는 엑셀 파일을 단순히 서버로 전송하는 것이 아니라, 파일 내부의 행 데이터를 파싱한 뒤 일정 단위로 나누어 서버에 전송해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;200&quot; data-start=&quot;163&quot; data-ke-size=&quot;size16&quot;&gt;프론트에서 데이터를 쪼개는 방식은 크게 두 가지로 볼 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;행 단위로 분할 하는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 SheetJs같은 클라이언트 파서를 준비해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;60&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mmRhX/dJMcafmxzkB/bhkHbuyTfOPy4krEeJPWg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mmRhX/dJMcafmxzkB/bhkHbuyTfOPy4krEeJPWg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mmRhX/dJMcafmxzkB/bhkHbuyTfOPy4krEeJPWg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmmRhX%2FdJMcafmxzkB%2FbhkHbuyTfOPy4krEeJPWg0%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;1438&quot; height=&quot;60&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;60&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 엑셀에 100만 건의 데이터가 있다면, 이를 1만 건씩 나누어 서버에 100번 요청하는 방식입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780317866068&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;100만 건 엑셀
&amp;rarr; 프론트에서 엑셀 파싱
&amp;rarr; 1만 건씩 분할
&amp;rarr; API 100번 호출
&amp;rarr; 서버 저장&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div data-is-intersecting=&quot;true&quot; data-turn-id-container=&quot;request-WEB:bec6f912-9c02-4546-8d91-8e4e81648eae-10&quot;&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5-thinking&quot; data-message-id=&quot;22289452-f5e1-440a-864b-3a8f02f221c5&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-end=&quot;474&quot; data-start=&quot;365&quot; data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 한 번에 서버로 전송되는 데이터 양을 줄일 수 있다는 점입니다.&lt;br /&gt;네트워크가 불안정하더라도 전체 100만 건이 한 번에 실패하는 것이 아니라, 일부 청크만 실패할 수 있습니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6j8w5/dJMcajbhcsj/Kx1VGsJ0Ky1aHeU1pYGAs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6j8w5/dJMcajbhcsj/Kx1VGsJ0Ky1aHeU1pYGAs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6j8w5/dJMcajbhcsj/Kx1VGsJ0Ky1aHeU1pYGAs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6j8w5%2FdJMcajbhcsj%2FKx1VGsJ0Ky1aHeU1pYGAs0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;756&quot; height=&quot;507&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;860&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;621&quot; data-start=&quot;476&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;위에서 프론트에서 Excel을 읽을 수 있어야 한다고 했기 때문에 SheetJs 같은 클라이언트 파서가 필요합니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;실행해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2908&quot; data-origin-height=&quot;134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cROcBM/dJMcahEApPW/0vxXIUXHD0HmN9bNWcFBkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cROcBM/dJMcahEApPW/0vxXIUXHD0HmN9bNWcFBkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cROcBM/dJMcahEApPW/0vxXIUXHD0HmN9bNWcFBkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcROcBM%2FdJMcahEApPW%2F0vxXIUXHD0HmN9bNWcFBkk%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;2908&quot; height=&quot;134&quot; data-origin-width=&quot;2908&quot; data-origin-height=&quot;134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;또한 업로드 진행 상황을 확인할 수 있다는 장점도 있습니다.&lt;br /&gt;그 이유는 데이터를 청크 단위로 나누어 전송하기 때문입니다. 각 청크가 서버로 전송될 때마다 API 호출 결과를 확인할 수 있으며, 이를 통해 전체 데이터 중 몇 건이 업로드되었는지 사용자에게 진행률 형태로 제공할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;732&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FcXMD/dJMcafz3Dnk/yvOyYLwvZ2uxr3hh8rAcsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FcXMD/dJMcafz3Dnk/yvOyYLwvZ2uxr3hh8rAcsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FcXMD/dJMcafz3Dnk/yvOyYLwvZ2uxr3hh8rAcsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFcXMD%2FdJMcafz3Dnk%2FyvOyYLwvZ2uxr3hh8rAcsK%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;643&quot; height=&quot;374&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;732&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;실행 시간을 확인해보니 다음과 같이 나왔습니다.&lt;/div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KmHyV/dJMcahLoJkB/ldQRrtNbN7DQ7GNtfgXHp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KmHyV/dJMcahLoJkB/ldQRrtNbN7DQ7GNtfgXHp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KmHyV/dJMcahLoJkB/ldQRrtNbN7DQ7GNtfgXHp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKmHyV%2FdJMcahLoJkB%2FldQRrtNbN7DQ7GNtfgXHp1%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;626&quot; height=&quot;276&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;약 총 337초 정도 걸린것을 확인할 수 있었습니다. 5분정도 걸렸습니다.&lt;br /&gt;테스트 결과, 아무런 분할 없이 데이터를 전송했을 때는 약 25분 정도가 소요되었습니다. 반면 데이터를 청크 단위로 분할하여 전송했을 경우 약 5분 내외로 처리되었으며, 이를 통해 전체 처리 시간이 약 &lt;u&gt;80% 감소&lt;/u&gt;한 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;30&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 보면 나쁘지 않아 보이지만, 단점도 명확합니다.&lt;/p&gt;
&lt;p data-end=&quot;168&quot; data-start=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;우선 브라우저가 100만 건의 엑셀 데이터를 직접 읽고 파싱해야 합니다. 데이터의 양이 많아질수록 클라이언트의 메모리 사용량도 함께 증가하게 되며, 경우에 따라서는 브라우저가 일시적으로 응답하지 않거나 화면이 멈추는 현상이 발생할 수 있습니다. &lt;br /&gt;결국, 서버의 부담을 줄일 수 있다는 장점은 있지만, 그 부담이 사용자 브라우저로 이동하는 구조라고 볼 수 있습니다. 특히 대용량 데이터를 처리해야 하는 환경에서는 사용자의 PC 성능이나 브라우저 상태에 따라 업로드 품질이 달라질 수 있다는 점도 고려해야 합니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;442&quot; data-start=&quot;296&quot; data-ke-size=&quot;size16&quot;&gt;ai를 시켜 벤치마킹을 해본결과&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;800&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c48ELi/dJMcahYUB20/m6kkijK53ME4kH3r8f8odk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c48ELi/dJMcahYUB20/m6kkijK53ME4kH3r8f8odk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c48ELi/dJMcahYUB20/m6kkijK53ME4kH3r8f8odk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc48ELi%2FdJMcahYUB20%2Fm6kkijK53ME4kH3r8f8odk%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;720&quot; height=&quot;207&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;442&quot; data-start=&quot;296&quot; data-ke-size=&quot;size16&quot;&gt;크롬이 살짝 빠른것을 알 수 있었습니다. 물론 테스트를 많이 해본것이 아니기 때문에 오차는 있을 수 있지만, 브라우저마다 속도가 다르다는 느낌은 받을 수 있었습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;442&quot; data-start=&quot;296&quot; data-ke-size=&quot;size16&quot;&gt;두 번째 방법은 &quot;이진 단위(바이트 단위)로 분할&quot;하는 방식입니다.&lt;/p&gt;
&lt;h3 data-end=&quot;442&quot; data-start=&quot;296&quot; data-ke-size=&quot;size23&quot;&gt;이진 단위로 분할하는 방식&lt;/h3&gt;
&lt;p data-end=&quot;801&quot; data-start=&quot;670&quot; data-ke-size=&quot;size16&quot;&gt;앞선 방식이 브라우저에서 엑셀 파일을 직접 파싱한 뒤, 데이터를 JSON 형태로 분할하여 서버에 전송하는 방식이었다면, 이번 방식은 엑셀 파일 자체를 바이트 단위로 나누어 서버에 전송하는 방식입니다.&lt;/p&gt;
&lt;p data-end=&quot;297&quot; data-start=&quot;113&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 file.slice()를 활용하면 파일을 1MB, 5MB와 같은 일정 크기의 조각으로 분할할 수 있습니다. 이후 분할된 파일 조각들을 서버로 전송하고, 서버에서는 이를 다시 하나의 파일로 재조립합니다. 재조립이 완료되면 Apache POI와 같은 라이브러리를 이용해 엑셀을 파싱하고 DB에 데이터를 저장하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KnWJh/dJMcabdiHSX/90n9GEU0boc01VCDkGXZKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KnWJh/dJMcabdiHSX/90n9GEU0boc01VCDkGXZKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KnWJh/dJMcabdiHSX/90n9GEU0boc01VCDkGXZKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKnWJh%2FdJMcabdiHSX%2F90n9GEU0boc01VCDkGXZKK%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;643&quot; height=&quot;317&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;434&quot; data-start=&quot;299&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;1438&quot; data-origin-height=&quot;60&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mmRhX/dJMcafmxzkB/bhkHbuyTfOPy4krEeJPWg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mmRhX/dJMcafmxzkB/bhkHbuyTfOPy4krEeJPWg0/img.png&quot; data-alt=&quot;delete 됨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mmRhX/dJMcafmxzkB/bhkHbuyTfOPy4krEeJPWg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmmRhX%2FdJMcafmxzkB%2FbhkHbuyTfOPy4krEeJPWg0%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;1438&quot; height=&quot;60&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;60&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;delete 됨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;653&quot; data-start=&quot;436&quot; data-ke-size=&quot;size16&quot;&gt;가장 큰 특징은 업로드가 바이트 단위로 이루어진다는 점입니다. 따라서 마지막으로 성공한 파일 조각 정보를 관리하고 있다면, 업로드 도중 네트워크 오류가 발생하더라도 처음부터 다시 전송할 필요 없이 실패한 지점부터 업로드를 재개할 수 있습니다. 실제로 Google Drive와 같은 대용량 파일 업로더나 AWS S3 Multipart Upload 역시 이와 유사한 방식을 사용하고 있습니다.&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;848&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w0ZOn/dJMcaciYOjD/YgZKGVqAUVkbeSUQIFwye1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w0ZOn/dJMcaciYOjD/YgZKGVqAUVkbeSUQIFwye1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w0ZOn/dJMcaciYOjD/YgZKGVqAUVkbeSUQIFwye1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw0ZOn%2FdJMcaciYOjD%2FYgZKGVqAUVkbeSUQIFwye1%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;570&quot; height=&quot;284&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;테스트 결과, 처리 시간은 &lt;u&gt;약 360초&lt;/u&gt;로 측정되었습니다. 이는 앞서 프론트에서 엑셀을 직접 파싱한 뒤 데이터를 분할하여 전송했던 방식보다 약 24초 정도 느린 결과였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이쯤 되니 브라우저의 메모리 사용량이 얼마나 차이 나는지도 궁금해집니다. 비록 처리 시간은 조금 더 소요되었지만, 사용자 입장에서 체감하는 경험은 오히려 더 나았기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 방식의 가장 큰 특징은 프론트만으로는 구현이 어렵다는 점입니다. 파일을 바이트 단위로 분할하여 전송하는 것은 프론트에서 수행할 수 있지만, 분할된 파일 조각들을 다시 하나의 파일로 재조립하는 작업은 서버에서 담당해야 합니다. 즉, 프론트와 백엔드가 함께 구현되어야만 완성할 수 있는 구조입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;994&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baXv67/dJMcafz4nUH/hclcokbuTFhd25biPMmECk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baXv67/dJMcafz4nUH/hclcokbuTFhd25biPMmECk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baXv67/dJMcafz4nUH/hclcokbuTFhd25biPMmECk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaXv67%2FdJMcafz4nUH%2FhclcokbuTFhd25biPMmECk%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;404&quot; height=&quot;314&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;994&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반면 프론트 입장에서는 상대적으로 부담이 적습니다. 브라우저가 엑셀 데이터를 직접 읽고 파싱할 필요가 없으며, 단순히 파일을 일정 크기의 조각으로 나누어 전송하기만 하면 됩니다. 따라서 대용량 엑셀 파일을 처리하는 과정에서도 브라우저가 멈추거나 과도한 메모리를 사용하는 현상이 상대적으로 적어, 사용자 입장에서는 보다 쾌적하게 동작한다고 느껴졌습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다만, 이 방식은 대용량 파일 전송의 안정성을 높이는 데 초점이 맞춰져 있습니다. &lt;u&gt;서버에 파일이 모두 업로드된 이후에는 여전히 파일 재조립, 엑셀 파싱, DB 적재 과정을 거쳐야 하므로 데이터 처리 성능 자체를 직접적으로 개선하는 방식은 아닙니다&lt;/u&gt;. 즉, 데이터 처리 최적화보다는 파일 업로드 안정성 향상에 더 적합한 방법이라고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 엑셀 업로드에 이 방식을 사용하는 것은 비효율적이라고 생각합니다. 엑셀 데이터는 결국 row 단위로 파싱되고 처리되어야 합니다. 따라서 바이트 단위로 파일을 분할하여 업로드하더라도, 서버에서는 다시 파일을 재조립한 뒤 row 단위로 파싱해야 합니다. 결국 업로드 안정성은 높일 수 있지만, 엑셀 데이터 처리 자체를 개선하는 방식은 아니라고 판단이 되어집니다.&lt;/p&gt;
&lt;h2 data-end=&quot;296&quot; data-start=&quot;238&quot; data-ke-size=&quot;size26&quot;&gt;서버에서 데이터를 쪼개보자.&lt;br /&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/h2&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;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvOKnx/dJMcaaZOmlJ/tZoV9kqwToKmrtykwObNu1/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvOKnx/dJMcaaZOmlJ/tZoV9kqwToKmrtykwObNu1/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvOKnx/dJMcaaZOmlJ/tZoV9kqwToKmrtykwObNu1/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvOKnx%2FdJMcaaZOmlJ%2FtZoV9kqwToKmrtykwObNu1%2Ftfile.svg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;300&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;164&quot; data-start=&quot;64&quot; data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 방식들은 데이터를 분할하는 주체가 프론트였습니다. 엑셀을 직접 파싱하여 JSON 형태로 분할하는 방법도 있었고, 파일 자체를 바이트 단위로 분할하는 방법도 있었습니다. 이번에는 관점을 바꿔보겠습니다. 프론트는 엑셀 파일을 그대로 서버에 전달하고, 데이터를 분할하는 작업은 서버에서 수행하는 방식입니다. 즉, 데이터를 쪼개는 주체가 프론트가 아닌 서버가 되는 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;164&quot; data-start=&quot;64&quot; data-ke-size=&quot;size16&quot;&gt;찾아보니 서버에서는 기술적으로 쪼개는 방법도 존재하지만, 프로토콜 또는 스펙 기반으로, 인프라 아키텍처 레벨로 , 언어/ JVM 기능적으로 쪼개는 방법들도 다양하게 존재합니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;164&quot; data-start=&quot;64&quot; 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;1000&quot; data-origin-height=&quot;625&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7eKyZ/dJMcafz4Nt9/3BuK4FZBpOBB2hIeaPZKP1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7eKyZ/dJMcafz4Nt9/3BuK4FZBpOBB2hIeaPZKP1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7eKyZ/dJMcafz4Nt9/3BuK4FZBpOBB2hIeaPZKP1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7eKyZ%2FdJMcafz4Nt9%2F3BuK4FZBpOBB2hIeaPZKP1%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;390&quot; height=&quot;244&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;625&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적으로 엑셀 데이터를 쪼개는 방법에 대해 고민해보았습니다. 크게 보면 처리량을 높이는 방법과, 대용량 데이터를 안정적으로 처리하는 방법으로 나누어 볼 수 있습니다. 이번에 적용해볼 기술은 Spring Batch입니다. &lt;u&gt;Spring Batch&lt;/u&gt;는 데이터를 일정한 chunk 단위로 나누어 처리할 수 있기 때문에 대량 데이터 적재에 적합하다고 판단하였습니다. 또한 Apache POI SAX도 함께 적용해보려고 하였지만, 확인해보니 이미 적용된 상태였습니다. &lt;u&gt;Apache POI SAX&lt;/u&gt;는 엑셀 전체를 메모리에 올리지 않고 row 단위로 읽는 방식이기 때문에, OOM이 발생하지 않았던 이유도 이와 관련이 있었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;spring batch&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bz04Me/dJMcaijfnkE/CAJfEKPk9951KJ8KzTzPS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bz04Me/dJMcaijfnkE/CAJfEKPk9951KJ8KzTzPS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bz04Me/dJMcaijfnkE/CAJfEKPk9951KJ8KzTzPS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbz04Me%2FdJMcaijfnkE%2FCAJfEKPk9951KJ8KzTzPS1%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;377&quot; height=&quot;212&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 프론트에서는 단순히 파일 업로드만 수행하고, 서버에서 Spring Batch를 이용해 엑셀 데이터를 분할 처리하기로 하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;263&quot; data-start=&quot;114&quot; data-ke-size=&quot;size16&quot;&gt;어떻게 보면 서버에서 엑셀 파일을 읽고 바로 저장하는 방식에 비해 row를 chunk 단위로 나누는 과정이 추가되었을 뿐입니다. 하지만 실제 테스트 결과 처리 시간에는 차이가 있었습니다. 약 25분 정도 소요되던 작업이 7분 내외로 단축된 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z62AD/dJMcaicw6al/nskOxAVZv22A1ZD63gJGdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z62AD/dJMcaicw6al/nskOxAVZv22A1ZD63gJGdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z62AD/dJMcaicw6al/nskOxAVZv22A1ZD63gJGdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz62AD%2FdJMcaicw6al%2FnskOxAVZv22A1ZD63gJGdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;397&quot; height=&quot;214&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 보면 프론트에서 데이터를 미리 분할하여 전송하는 방식이 가장 좋아 보일 수 있습니다. 하지만 서버에서 row를 분할하여 처리하는 방식도 분명한 장점이 존재합니다. &lt;u&gt;(336s -&amp;gt; 360s -&amp;gt; 420s)&lt;/u&gt;&lt;/p&gt;
&lt;p data-end=&quot;574&quot; data-start=&quot;367&quot; data-ke-size=&quot;size16&quot;&gt;가장 큰 장점은 처리 현황을 서버에서 직접 관리할 수 있다는 점입니다. 프론트에서 데이터를 분할하여 전송하는 방식은 업로드 진행률은 확인할 수 있지만, 실제 데이터가 얼마나 저장되었는지는 확인하기 어려웠습니다. 반면, 서버에서 Spring Batch를 통해 데이터를 처리하는 경우에는 현재 몇 건이 처리되었고, 몇 건이 남았는지와 같은 실제 처리 현황을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kLVjG/dJMcafthIeU/UP4Cu9ucUJPOSFJCEEmoHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kLVjG/dJMcafthIeU/UP4Cu9ucUJPOSFJCEEmoHk/img.png&quot; data-alt=&quot;파일을 읽고 db에 쓰기 작업을 가집니다. procress는 사용할 팔요가 없어 의도적으로 사용하지 않았습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kLVjG/dJMcafthIeU/UP4Cu9ucUJPOSFJCEEmoHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkLVjG%2FdJMcafthIeU%2FUP4Cu9ucUJPOSFJCEEmoHk%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;722&quot; height=&quot;261&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파일을 읽고 db에 쓰기 작업을 가집니다. procress는 사용할 팔요가 없어 의도적으로 사용하지 않았습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;657&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;즉, 단순히 파일 업로드 진행 여부가 아니라 실제 데이터 적재 진행 상황을 확인할 수 있다는 점은 운영 관점에서도 의미가 있다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 1.44em; letter-spacing: -1px;&quot;&gt;언어 관점&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Swt3p/dJMcacDohqY/kizKfFRfHVaCvm16BDdg01/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Swt3p/dJMcacDohqY/kizKfFRfHVaCvm16BDdg01/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Swt3p/dJMcacDohqY/kizKfFRfHVaCvm16BDdg01/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSwt3p%2FdJMcacDohqY%2FkizKfFRfHVaCvm16BDdg01%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;142&quot; height=&quot;264&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1860&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; data-start=&quot;64&quot; data-end=&quot;164&quot;&gt;현재는 서버(JVM)에서 엑셀 데이터를 읽고 처리한 뒤 DB에 저장하는 구조를 사용하고 있습니다. 한 행씩 순차적으로 진행되는 이 흐름을 JVM 차원에서 어떻게 쪼갤 수 있는지 InputStream을 통한 스트리밍 입력, ExecutorService를 통한 병렬 처리, Parallel Stream을 통한 선언적 분산 등의 도구를 중심으로 정리합니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-start=&quot;64&quot; data-end=&quot;164&quot;&gt;InputStream을 통한 스트리밍 입력&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 부분은 이미 적용이 되어 있습니다. 그렇다면, 이 방식으로 왜 적용해야 하는지 생각해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MultipartFile.getInputStream()으로&amp;nbsp;받은&amp;nbsp;입력을&amp;nbsp;그대로&amp;nbsp;POI에&amp;nbsp;흘려보내고,&amp;nbsp;POI는&amp;nbsp;한&amp;nbsp;행씩&amp;nbsp;SAX&amp;nbsp;이벤트로&amp;nbsp;파싱합니다.&amp;nbsp;메모리에&amp;nbsp;파일&amp;nbsp;전체를&amp;nbsp;올리지&amp;nbsp;않습니다.&lt;br /&gt;&lt;br /&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;1424&quot; data-origin-height=&quot;868&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dOu7a4/dJMcaiwNsBc/XglIcd7B5RZc12qKKGNka1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dOu7a4/dJMcaiwNsBc/XglIcd7B5RZc12qKKGNka1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dOu7a4/dJMcaiwNsBc/XglIcd7B5RZc12qKKGNka1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdOu7a4%2FdJMcaiwNsBc%2FXglIcd7B5RZc12qKKGNka1%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;520&quot; height=&quot;317&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;868&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 단계에서 메모리가 부풀어 오릅니다.&lt;br /&gt;&lt;br /&gt;1. getBytes() &amp;mdash; 파일 크기만큼 byte[] 점유 (28MB)&lt;br /&gt;2. WorkbookFactory.create() &amp;mdash; 모든 셀을 객체로 메모리에 보관 (수 GB 가능)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100만 행짜리 파일이면 JVM 힙 한도를 쉽게 넘겨 OutOfMemoryError가 발생합니다. 28MB &amp;rarr; 28MB가 아니라, 28MB &amp;rarr; 수 GB로 부풀어 오를 수 있습니다. xlsx는 압축 포맷이라 압축 해제 시 5~10배로 커지고, 그걸 모두 객체로 다시 표현하면 또 한 차례 늘어나기 때문입니다.&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;874&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7LqAP/dJMcabxEc7l/xYGjd38ISWuoZf0213Mod0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7LqAP/dJMcabxEc7l/xYGjd38ISWuoZf0213Mod0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7LqAP/dJMcabxEc7l/xYGjd38ISWuoZf0213Mod0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7LqAP%2FdJMcabxEc7l%2FxYGjd38ISWuoZf0213Mod0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;421&quot; height=&quot;287&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 예상과달리 oom이 발생하지 않았습니다. 이는 POI라이브러리가 자체 안전장치로 100MB 이상 배열할당을 거부합니다. &lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;i&gt;(POI 5.2.1에서 추가)&lt;/i&gt;&lt;/span&gt;&lt;br /&gt;POI 라이브러리가 이런 가드를 만든 이유는 xlsx 파일은 zip으로 압축되어 있어서 28MB 파일을 압축 해제 시 수 GB로 부풀 수 있습니다. 그렇게 되면 악의적인 zip bomb 공격이 가능해집니다. 만약, DOM 파서가 그걸 그대로 메모리에 올린다면, 서버는 OOM이 발생하게 됩니다. 그래서 POI는 이만큼 이상은 위험하니 거부 한도를 내장하였습니다. 물론, &lt;u&gt;IOUtils.setByteArrayMaxOverride()&lt;/u&gt;을 통해 강제로 늘릴 수 있수 있지만, 그 길로 가면 진짜 OOM이 발생할 수 있다고 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ExecutorService를 통한 병렬 처리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExecutorService를 통한 병렬 처리도 검토해보았습니다. Spring Batch를 사용하면 chunk 단위 처리와 함께 병렬 처리도 구성할 수 있습니다. 하지만 그 전에 순수 Java 관점에서도 데이터를 병렬로 처리할 수 있는지 확인해보고 싶었습니다. 현재 서버에서는 InputStream을 통해 엑셀 파일을 읽고 있습니다. 그렇다면 InputStream으로 읽어온 데이터를 일정 단위로 나눈 뒤, ExecutorService를 이용해 병렬로 처리하면 성능이 개선될 수 있을까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;852&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ppbe2/dJMcacXD4mH/ZwIdej7FZZ9W3r43GyJOW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ppbe2/dJMcacXD4mH/ZwIdej7FZZ9W3r43GyJOW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ppbe2/dJMcacXD4mH/ZwIdej7FZZ9W3r43GyJOW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPpbe2%2FdJMcacXD4mH%2FZwIdej7FZZ9W3r43GyJOW1%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;542&quot; height=&quot;312&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;852&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;381&quot; data-start=&quot;311&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;884&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n1zwn/dJMcahxUcl6/3NlZgQ9FAMkFhY3TahIgU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n1zwn/dJMcahxUcl6/3NlZgQ9FAMkFhY3TahIgU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n1zwn/dJMcahxUcl6/3NlZgQ9FAMkFhY3TahIgU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn1zwn%2FdJMcahxUcl6%2F3NlZgQ9FAMkFhY3TahIgU1%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;457&quot; height=&quot;247&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상 밖의 결과였습니다. Spring Batch를 사용하지 않았음에도 처리 시간이 &lt;u&gt;약 268.2초&lt;/u&gt;로 측정되었고, 지금까지 테스트한 방식 중 가장 빠른 결과였습니다.&lt;/p&gt;
&lt;p data-end=&quot;285&quot; data-start=&quot;113&quot; data-ke-size=&quot;size16&quot;&gt;아무래도 데이터를 일정 단위로 나눈 뒤 ExecutorService를 통해 여러 스레드에서 병렬로 처리하면서 전체 처리 시간이 줄어든 것으로 보입니다. 즉, 하나의 스레드가 모든 데이터를 순차적으로 처리하는 방식보다, 여러 스레드가 작업을 나누어 처리한 것이 성능 개선에 영향을 준 것으로 판단하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 스레드가 2~20까지 테스트한 결과는 어떨까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbMlbR/dJMcacDooKb/3UzOnH6TxSkGJfdskz39j1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbMlbR/dJMcacDooKb/3UzOnH6TxSkGJfdskz39j1/img.png&quot; data-alt=&quot;스레드 풀 4개 짜리의 시간이 다른 이유는 재 테스트를 진행하였습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbMlbR/dJMcacDooKb/3UzOnH6TxSkGJfdskz39j1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbMlbR%2FdJMcacDooKb%2F3UzOnH6TxSkGJfdskz39j1%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;619&quot; height=&quot;530&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;808&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스레드 풀 4개 짜리의 시간이 다른 이유는 재 테스트를 진행하였습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 결과, 스레드 수가 6개가 되는 시점부터 처리 시간이 급격하게 단축되는 것을 확인할 수 있었습니다. 이는 일정 수준까지는 병렬 처리 효과가 분명히 존재한다는 의미로 볼 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;248&quot; data-start=&quot;132&quot; data-ke-size=&quot;size16&quot;&gt;하지만 스레드 수가 10개를 넘어가면서부터는 처리 시간이 안정적으로 감소하지 않고, 오히려 증가와 감소를 반복하는 모습을 보였습니다. 즉, 스레드 수를 늘린다고 해서 무조건 성능이 좋아지는 것은 아니었습니다.&lt;/p&gt;
&lt;p data-end=&quot;409&quot; data-start=&quot;253&quot; data-ke-size=&quot;size16&quot;&gt;특히 스레드 풀 크기를 16개로 설정했을 때는 알 수 없는 이유로 작업이 중단되었습니다. 원인을 확인해보니 DB 연결이 끊어진 것이 문제였습니다. 여러 스레드가 동시에 insert를 수행하면서 Oracle 쪽 세션이 종료되었고, 그 결과 chunk 작업에서 예외가 발생하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2520&quot; data-origin-height=&quot;1294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qGBFm/dJMcabdkMpx/t1c4Lo8J7DUeHo1f28edxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qGBFm/dJMcabdkMpx/t1c4Lo8J7DUeHo1f28edxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qGBFm/dJMcabdkMpx/t1c4Lo8J7DUeHo1f28edxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqGBFm%2FdJMcabdkMpx%2Ft1c4Lo8J7DUeHo1f28edxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;329&quot; data-origin-width=&quot;2520&quot; data-origin-height=&quot;1294&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;496&quot; data-start=&quot;414&quot; data-ke-size=&quot;size16&quot;&gt;이후 Future.get()을 통해 예외가 전파되면서 서버에서는 500 에러가 발생하였고, 최종적으로 curl 응답 파싱 실패로 기록되었습니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;641&quot; data-start=&quot;501&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 병렬 처리에는 적정 수준이 존재한다는 것을 확인할 수 있었습니다. 스레드 수를 늘리면 일정 구간까지는 처리량이 증가하지만, DB 커넥션 수, Oracle 세션 제한, insert 부하 등을 넘어서면 오히려 실패 가능성이 높아질 수 있습니다. 이는 ExecutorService 자체의 문제가 아니라, HikariCP 커넥션 풀에서 병목이 발생했던 것이 원인이었습니다. 여러 스레드가 동시에 DB insert를 수행하려고 했지만, 실제로 사용할 수 있는 DB 커넥션 수에는 제한이 있었기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;188&quot; data-start=&quot;165&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 이를 어떻게 해결할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 방법은 HikariCP 커넥션 풀 크기를 스레드 풀 크기에 맞춰 늘리는 것입니다. 예를 들어 작업 스레드가 16개라면, DB 커넥션도 16개 이상 사용할 수 있도록 설정하는 방식입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnPgi8/dJMcadhZJ3R/jkDMUkHMfD3rh5VIEkUnIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnPgi8/dJMcadhZJ3R/jkDMUkHMfD3rh5VIEkUnIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnPgi8/dJMcadhZJ3R/jkDMUkHMfD3rh5VIEkUnIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnPgi8%2FdJMcadhZJ3R%2FjkDMUkHMfD3rh5VIEkUnIk%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;768&quot; height=&quot;108&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;339&quot; data-start=&quot;162&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 방법은 권장하기 어렵습니다. 커넥션 풀 크기를 늘리면 애플리케이션 입장에서는 대기 시간이 줄어들 수 있지만, 그만큼 DB가 동시에 처리해야 하는 요청 수가 증가합니다. 결국 병목이 애플리케이션에서 DB로 이동할 뿐이며, DB 세션 수나 insert 부하를 초과하면 오히려 장애 가능성이 높아질 수 있습니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;432&quot; data-start=&quot;344&quot; data-ke-size=&quot;size16&quot;&gt;따라서 커넥션 풀을 무작정 늘리기보다는, DB가 안정적으로 감당할 수 있는 수준에서 스레드 수와 커넥션 수를 함께 조정하는 것이 더 적절하다고 판단하였습니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;432&quot; data-start=&quot;344&quot; data-ke-size=&quot;size16&quot;&gt;하지만, 이 글에서는 테스트를 위해 HikariCP 커넥션 풀을 20으로 올려보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;1120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhI12z/dJMcaiDAfwN/JdDkDfoobcXMAOvx0aCHtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhI12z/dJMcaiDAfwN/JdDkDfoobcXMAOvx0aCHtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhI12z/dJMcaiDAfwN/JdDkDfoobcXMAOvx0aCHtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhI12z%2FdJMcaiDAfwN%2FJdDkDfoobcXMAOvx0aCHtk%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;390&quot; height=&quot;337&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;1120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 스레드 2개를 기준으로 얼만큼 성능이 향상 되었는지도 파악해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;1090&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/227TP/dJMcaaZP9k7/0renQt9KuMsloqRUBxlVmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/227TP/dJMcaaZP9k7/0renQt9KuMsloqRUBxlVmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/227TP/dJMcaaZP9k7/0renQt9KuMsloqRUBxlVmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F227TP%2FdJMcaaZP9k7%2F0renQt9KuMsloqRUBxlVmk%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;363&quot; height=&quot;310&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;1090&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 확인해보니 가장 안정적이면서 성능이 우수했던 구간은 스레드 수가 8개일 때였습니다. 가장 빠른 처리 시간은 스레드 12개에서 측정되었지만, 스레드 10개 구간에서 오히려 성능이 감소하는 현상이 나타났기 때문에 안정적인 구간이라고 보기는 어려웠습니다.&lt;/p&gt;
&lt;p data-end=&quot;256&quot; data-start=&quot;173&quot; data-ke-size=&quot;size16&quot;&gt;결국 단순히 스레드 수를 늘리는 것이 성능 향상으로 이어지는 것은 아니었으며, 적절한 스레드 수를 찾는 과정이 중요하다는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;437&quot; data-start=&quot;261&quot; data-ke-size=&quot;size16&quot;&gt;이번 테스트를 통해 ExecutorService를 활용한 병렬 처리 방법과, 병렬 처리 과정에서 발생할 수 있는 커넥션 풀 병목에 대해 함께 학습할 수 있었습니다. 또한 성능 최적화는 단순히 스레드를 늘리는 것이 아니라, 애플리케이션과 DB가 감당할 수 있는 균형점을 찾는 과정이라는 점도 확인할 수 있었습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 1.44em; letter-spacing: -1px;&quot;&gt;프로토콜 관점&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;br /&gt;현재 업로드는 HTTP Multipart 기반입니다&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;. 하지만 네트워크 장애로 업로드가 실패하면, 서버에 수신 상태가 남지 않아 처음부터 다시 올려야 합니다. 이때 재개 가능한 업로드 프로토콜인 tus를 적용하면, 끊긴 지점부터 이어서 전송할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-end=&quot;437&quot; data-start=&quot;261&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;tus&lt;/span&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1780667027732&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;tus - resumable file uploads&quot; data-og-description=&quot;tus is the open protocol standard for resumable and reliable file uploads across the web, facilitating efficient and seamless file transfer experiences.&quot; data-og-host=&quot;tus.io&quot; data-og-source-url=&quot;https://tus.io/&quot; data-og-url=&quot;https://tus.io/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dnko5O/dJMb86n5eC4/sKUPDktkMZ32JULBLNn3RK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bdmUN0/dJMb8SXFvMN/nszAnENr1L9KIWIiKVvJ41/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bCU5No/dJMb83kADeQ/jldxFW8LKP1KlsE9xoixyK/img.png?width=1116&amp;amp;height=478&amp;amp;face=0_0_1116_478&quot;&gt;&lt;a href=&quot;https://tus.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tus.io/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dnko5O/dJMb86n5eC4/sKUPDktkMZ32JULBLNn3RK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bdmUN0/dJMb8SXFvMN/nszAnENr1L9KIWIiKVvJ41/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bCU5No/dJMb83kADeQ/jldxFW8LKP1KlsE9xoixyK/img.png?width=1116&amp;amp;height=478&amp;amp;face=0_0_1116_478');&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;tus - resumable file uploads&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;tus is the open protocol standard for resumable and reliable file uploads across the web, facilitating efficient and seamless file transfer experiences.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tus.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tus는 생각보다 다양한 기능을 제공하기 때문에 모든 내용을 다루기에는 범위가 너무 넓습니다. 따라서 이번 글에서는 핵심 동작 원리와 간단한 구현 방법만 살펴보겠습니다.&lt;/p&gt;
&lt;p data-end=&quot;304&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;구현은 가장 간단한 CDN 방식을 이용하여 진행하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1346&quot; data-origin-height=&quot;62&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPLsi1/dJMcad3ifyl/dCl7cA2AG423LV5x3uQRUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPLsi1/dJMcad3ifyl/dCl7cA2AG423LV5x3uQRUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPLsi1/dJMcad3ifyl/dCl7cA2AG423LV5x3uQRUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPLsi1%2FdJMcad3ifyl%2FdCl7cA2AG423LV5x3uQRUK%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;1346&quot; height=&quot;62&quot; data-origin-width=&quot;1346&quot; data-origin-height=&quot;62&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tus프로토콜을 이용하면 서버에 얼마나 적재중인지 확인할 수 있습니다.&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;452&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EJm0I/dJMcahkqLNV/sJGgMMu7vSrxRXVfEwoNzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EJm0I/dJMcahkqLNV/sJGgMMu7vSrxRXVfEwoNzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EJm0I/dJMcahkqLNV/sJGgMMu7vSrxRXVfEwoNzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEJm0I%2FdJMcahkqLNV%2FsJGgMMu7vSrxRXVfEwoNzk%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;382&quot; height=&quot;49&quot; data-origin-width=&quot;452&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, db에 적재가 되어지는것이 아니기 때문에 서버 메모리에 저장이 되어지는것으로 추정이 되어집니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pDh0u/dJMcai4AsTc/x42h5Aem9LKDrJr599n8ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pDh0u/dJMcai4AsTc/x42h5Aem9LKDrJr599n8ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pDh0u/dJMcai4AsTc/x42h5Aem9LKDrJr599n8ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpDh0u%2FdJMcai4AsTc%2Fx42h5Aem9LKDrJr599n8ek%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;632&quot; height=&quot;325&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해보니 성능 측면에서는 기대했던 만큼의 차이를 확인할 수는 없었습니다. 다만 tus 프로토콜을 사용하는 가장 큰 이유는 성능이 아니라 전송 재개(Resumable Upload) 에 있습니다. 네트워크 문제가 발생하더라도 처음부터 다시 업로드하는 것이 아니라, 중단된 지점부터 업로드를 이어갈 수 있다는 점이 가장 큰 특징입니다.&lt;/p&gt;
&lt;p data-end=&quot;351&quot; data-start=&quot;227&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;880&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSiQGu/dJMcaf078nG/PNblJo0tu8AQ7w4Q7c4GeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSiQGu/dJMcaf078nG/PNblJo0tu8AQ7w4Q7c4GeK/img.png&quot; data-alt=&quot;추후 tus에 대해 조금더 자세하게 학습해보겠습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSiQGu/dJMcaf078nG/PNblJo0tu8AQ7w4Q7c4GeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSiQGu%2FdJMcaf078nG%2FPNblJo0tu8AQ7w4Q7c4GeK%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;669&quot; height=&quot;228&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;추후 tus에 대해 조금더 자세하게 학습해보겠습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;426&quot; data-start=&quot;356&quot; data-ke-size=&quot;size16&quot;&gt;따라서 이번 글에서는 tus의 핵심 개념과 동작 원리 정도만 소개하고, 상세한 구현 내용은 추후 별도로 다뤄보도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;296&quot; data-start=&quot;238&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;DB에서 데이터를 쪼개보자.&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bM9nMe/dJMcacDpHMs/n3RN2Ph8vdOPItpXzWBId0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bM9nMe/dJMcacDpHMs/n3RN2Ph8vdOPItpXzWBId0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bM9nMe/dJMcacDpHMs/n3RN2Ph8vdOPItpXzWBId0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbM9nMe%2FdJMcacDpHMs%2Fn3RN2Ph8vdOPItpXzWBId0%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;225&quot; height=&quot;225&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;300&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-end=&quot;111&quot; data-start=&quot;76&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 DB에서 데이터를 분할하는 것은 어떤 의미가 있을까요?&lt;/p&gt;
&lt;p data-end=&quot;211&quot; data-start=&quot;116&quot; data-ke-size=&quot;size16&quot;&gt;사실 DB에서 데이터를 분할한다고 해서 파일 업로드 속도가 빨라지거나, 서버가 데이터를 읽는 속도가 개선되는 것은 아닙니다. 즉, 전송 과정의 최적화와는 거리가 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;251&quot; data-start=&quot;216&quot; data-ke-size=&quot;size16&quot;&gt;그럼에도 불구하고 DB에서 데이터를 분할하는 이유는 무엇일까요?&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;375&quot; data-start=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 처리량보다는 &lt;b&gt;안정성&lt;/b&gt;에 있습니다. 트랜잭션 범위를 줄이고, DB 부하를 분산하며, 실패 시 재처리 범위를 최소화할 수 있기 때문입니다. 그렇다면 DB에서는 어떤 방식으로 데이터를 분할할 수 있을까요?&lt;/p&gt;
&lt;h3 data-end=&quot;375&quot; data-start=&quot;256&quot; data-ke-size=&quot;size23&quot;&gt;프로시저&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 데이터를 분할하는 방법으로는 여러 가지가 있지만, 이번 글에서는 프로시저를 중심으로 살펴보겠습니다. 프로시저를 사용하면 서버가 임시 테이블에 데이터를 적재한 뒤, DB 내부에서 일정 단위로 데이터를 검증하고 실제 테이블에 반영할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK0WVr/dJMcafUqt3o/RrEoKGk4mgPAJq1sFey89k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK0WVr/dJMcafUqt3o/RrEoKGk4mgPAJq1sFey89k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK0WVr/dJMcafUqt3o/RrEoKGk4mgPAJq1sFey89k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK0WVr%2FdJMcafUqt3o%2FRrEoKGk4mgPAJq1sFey89k%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;499&quot; height=&quot;264&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시나 처리량은 줄지 않았군요. 프로시저 쿼리를 보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1762&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qcrre/dJMcagsenCY/bmtreKlnvwjv0jDGw94040/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qcrre/dJMcagsenCY/bmtreKlnvwjv0jDGw94040/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qcrre/dJMcagsenCY/bmtreKlnvwjv0jDGw94040/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqcrre%2FdJMcagsenCY%2FbmtreKlnvwjv0jDGw94040%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;1762&quot; height=&quot;328&quot; data-origin-width=&quot;1762&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로시저는 user_row_tab 형태의 여러 사용자 데이터를 입력받아 users 테이블에 한 번에 저장하는 역할을 합니다. 여기서 핵심은 FORALL입니다. 일반적인 FOR LOOP는 한 건씩 SQL을 실행하지만, FORALL은 PL/SQL 컬렉션에 담긴 데이터를 SQL 엔진에 한 번에 전달하여 대량 INSERT를 수행할 수 있습니다. 즉, 서버에서 100만 건을 한 건씩 DB에 전달하는 것이 아니라, 일정 단위로 묶은 데이터를 프로시저에 전달하고 DB 내부에서 일괄 처리하는 방식입니다. 이를 통해 애플리케이션과 DB 사이의 반복 호출을 줄일 수 있고, 대량 데이터 적재 시 발생하는 오버헤드를 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;632&quot; data-start=&quot;482&quot; data-ke-size=&quot;size16&quot;&gt;다만 이 구조는 단순 INSERT에 가깝습니다. 실제 운영 환경에서는 여기서 한 단계 더 발전시킬 수 있습니다. 예를 들어 중복 데이터 검증, 필수값 누락 검증, 실패 데이터 별도 저장, 처리 결과 건수 반환, 예외 발생 시 로그 테이블 적재 등을 추가할 수 있습니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;759&quot; data-start=&quot;637&quot; data-ke-size=&quot;size16&quot;&gt;결국 프로시저 방식의 장점은 DB 내부에서 대량 데이터를 빠르게 반영할 수 있다는 점이고, 발전 방향은&lt;u&gt; 단순 저장을 넘어 검증, 실패 관리, 결과 추적이 가능한 적재 프로세스로 만드는 것이라고 볼 수 있습니다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;759&quot; data-start=&quot;637&quot; data-ke-size=&quot;size16&quot;&gt;다만 프로시저가 제공하는 장점 중 상당수는 서버에서도 대체할 수 있습니다. 데이터 검증, 실패 처리, 배치 Insert, 트랜잭션 제어는 Java와 Spring 기반에서도 충분히 구현할 수 있습니다. 따라서 프로시저는 반드시 필요한 선택이라기보다는, DB 내부에서 처리하는 것이 더 적합한 환경에서 선택할 수 있는 방식이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;759&quot; data-start=&quot;637&quot; data-ke-size=&quot;size16&quot;&gt;간단하게 DB는 이 정도만 알아보도록 하겠습니다. DB파트는 DB에서도 데이터 분할이 가능한지 체크하기 위함이었습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;759&quot; data-start=&quot;637&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;원래 목적은 프론트, 서버, DB에서 엑셀 row를 어떻게 분할하여 처리할 수 있는지 간단하게 정리하는 것이었습니다. &lt;/span&gt;&lt;span&gt;하지만 생각보다 다양한 방법들을 새롭게 알게 되었습니다. 예를 들어 프론트에서 데이터를 row 단위로 분할하여 전송하는 방법, 파일을 바이트 단위로 분할하여 전송하는 방법, tus 프로토콜을 이용한 전송 재개 방식, 그리고 ExecutorService를 활용한 병렬 처리 방식 등이 있었습니다. &lt;/span&gt;&lt;span&gt;이번에 테스트한 방법들이 모두 대용량 엑셀 업로드에 적합한 것은 아니었습니다. 엑셀 업로드는 결국 파일을 읽고 row 단위로 처리해야 하기 때문에, 바이트 단위 분할과 같은 방식은 목적에 비해 다소 우회적인 접근이라고 느껴졌습니다. 하지만 이런 방식도 존재한다는 것을 알게 되었고, 언젠가 다른 문제를 해결할 때 활용할 수 있을 것이라 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한, 이번 글에서 소개한 방법들이 전부는 아닙니다. 조금만 더 깊게 들어가도 훨씬 다양한 기술과 아키텍처를 확인할 수 있었습니다. 다만 모든 내용을 담기에는 범위가 너무 넓고, 실제 엑셀 업로드 문제를 해결하는 데 필요한 수준을 넘어선다고 판단하여 제외하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결과적으로, 이번 글을 통해 느낀 점은 단순히 엑셀 업로드 기능 하나를 구현하는 것이 아니라, 데이터를 어디에서 분할할 것인지에 따라 전송 방식, 처리량, 안정성, 운영 방식까지 달라질 수 있다는 점이었습니다. 앞으로도 새로운 요구사항을 마주하게 된다면 구현에만 집중하기보다는 어떤 위치에서 문제를 해결하는 것이 가장 적절한지 먼저 고민해보려고 합니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발/이력관리</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/582</guid>
      <comments>https://b-programmer.tistory.com/582#entry582comment</comments>
      <pubDate>Sat, 6 Jun 2026 16:03:30 +0900</pubDate>
    </item>
    <item>
      <title>뉴럴 네트워크</title>
      <link>https://b-programmer.tistory.com/581</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존 언어 모델 방식 중 하나인 N-gram은 이전 데이터를 기반으로 다음 단어를 예측하는 방식입니다. 하지만 N의 크기가 너무 낮다면 문맥을 충분히 이해하지 못해 의도를 알 수 없는 문장을 생성할 수 있습니다. 반대로 N의 크기가 너무 높아진다면 문맥 자체는 자연스러워지지만, 희소성 문제가 심해져 새로운 문장을 생성할 가능성이 낮아지게 됩니다. 이러한 한계를 해결하기 위해 다양한 연구가 진행되었고, 2003년 조수아 벤지오 교수의 신경망 기반 언어 모델 연구가 큰 전환점이 되었습니다. 특히 단어를 벡터로 표현하고 신경망을 통해 다음 단어를 예측하는 방식은 이후 현대 LLM의 기반이 되는 흐름으로 이어지게 됩니다. 이번 글에서는 이러한 흐름 속에서 등장한 뉴럴 네트워크가 무엇인지에 대해 알아보겠습니다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;뉴럴 네트워크란?&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;뉴럴 네트워크는 인간의 뇌 신경망 구조에서 아이디어를 얻어 만들어진 모델입니다. 인간은 뉴런(neuron)들이 서로 연결되어 정보를 처리합니다. 이러한 구조를 컴퓨터에서도 비슷하게 표현하여 데이터를 학습하고 패턴을 분석할 수 있도록 만든 것이 뉴럴 네트워크입니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y0aWf/dJMcahdtA6i/rqeRdKQFdRC7bXYH6N3Pb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y0aWf/dJMcahdtA6i/rqeRdKQFdRC7bXYH6N3Pb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y0aWf/dJMcahdtA6i/rqeRdKQFdRC7bXYH6N3Pb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY0aWf%2FdJMcahdtA6i%2FrqeRdKQFdRC7bXYH6N3Pb1%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;446&quot; height=&quot;297&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 뉴럴 네트워크는 실제로 어떤 방식으로 데이터를 학습하게 될까요?&lt;br&gt;단순한 이론보다는 흐름을 보는 것이 이해가 더 쉽다고 생각하여 간단한 영상을 준비해보았습니다.&lt;/p&gt;&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=MUf3hiQ5TB4&quot; data-video-thumbnail=&quot;https://blog.kakaocdn.net/dna/bgJdPy/dJMb8QeskH8/AAAAAAAAAAAAAAAAAAAAALI6P_pZOhy3pzYQNiRP23X1xrw-zxWrosmR9DwZiwjQ/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1780239599&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=wornqFLo6RbixMekANYZDBcoBl8%3D&quot; data-video-width=&quot;480&quot; data-video-height=&quot;360&quot; data-video-origin-width=&quot;480&quot; data-video-origin-height=&quot;360&quot; data-video-title=&quot;뉴럴 네트워크 학습&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/MUf3hiQ5TB4&quot; width=&quot;480&quot; height=&quot;360&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 어떤 단어가 정답인지 제대로 학습되지 않았기 때문에 여러 글자들이 비슷한 확률로 출력되고 있으며, loss 값 역시 높게 측정됩니다. 하지만 학습이 반복될수록 데이터의 패턴을 점차 학습하게 되고, 최종적으로는 &quot;다&quot;라는 글자가 가장 높은 확률로 예측되는 것을 확인할 수 있습니다.&amp;nbsp;&lt;br&gt;여기서 한 가지 의문이 생깁니다.&lt;br&gt;&quot;사, 랑, 하&quot;라는 문자와 &quot;다, 는, 늙, 기&quot;라는 문자 사이의 관계를 컴퓨터는 어떻게 이해할 수 있을까요?&lt;br&gt;중요한 점은 컴퓨터는 인간처럼 문자 자체의 의미를 이해하지 못한다는 것입니다. 컴퓨터 입장에서는 단순한 데이터에 불과하기 때문입니다.&lt;br&gt;이를 해결하기 위해 문자를 숫자로 변환하여 표현하기 시작하였고, 숫자를 기반으로 특정 위치(좌표)에 배치하여 문자 간의 관계를 학습할 수 있도록 하였습니다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;좌표를 만드는건 알겠는데 어떤 기준으로 좌표를 만드는가?&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;사&quot;라는 문자가 정확히 어떤 숫자나 의미를 가지는지는 초반에는 알 수 없습니다. 실제로 뉴럴 네트워크는 처음부터 의미를 이해하고 있는 것이 아니라, 랜덤한 값에서 시작하여 점차 학습을 통해 관계를 찾아가게 됩니다.&lt;br&gt;그렇기 때문에 초기에는 예측 결과가 불안정하며 여러 단어들이 비슷한 확률로 출력될 수 있습니다. 이후 학습이 반복되면서 특정 패턴의 확률이 점점 높아지게 됩니다.&lt;br&gt;흔히 LLM이 &quot;주사위를 굴려서 답변한다&quot;라고 표현하는 이유 역시, 내부적으로 확률 기반 예측을 수행하기 때문입니다. &lt;br&gt;컴퓨터가 문자 간의 관계를 이해할 수 있도록 숫자 형태의 좌표로 변환하는 과정을&lt;b&gt; 임베딩&lt;/b&gt;(Embedding)이라고 합니다.&lt;/p&gt;&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=D3gWN9YZdGY&quot; data-video-thumbnail=&quot;https://blog.kakaocdn.net/dna/eWxhq/dJMb9aKLc0a/AAAAAAAAAAAAAAAAAAAAAE1CWlRLALbLoMHzqXEGYYIqYdw9GKxp5WZ0vnEn3oMc/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1780239599&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=vUFE8G%2FKCYhzmaLxrlsotRGat%2FM%3D&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-video-title=&quot;임베딩 학습&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/D3gWN9YZdGY&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 임베딩은 글자들을 숫자 좌표로 변환하는 과정을 의미합니다. 보통 이러한 숫자 변환은 벡터(Vector) 형태의 좌표로 표현됩니다.&lt;br&gt;하지만 임베딩만 했다고 해서 학습이 끝난 것은 아닙니다. 첫 번째 영상을 다시 보면, 앞 글자를 확인하고 숫자로 변환한 뒤 은닉층(Hidden Layer)과 출력층(Output Layer)으로 이동하는 것을 알 수 있습니다.&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;벡터(Vector)란 여러 숫자를 하나의 좌표처럼 묶어서 표현한 값을 의미&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;1260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAzQyd/dJMb997zkav/1TFkaCyN8Y0fKTO2KX5iY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAzQyd/dJMb997zkav/1TFkaCyN8Y0fKTO2KX5iY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAzQyd/dJMb997zkav/1TFkaCyN8Y0fKTO2KX5iY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAzQyd%2FdJMb997zkav%2F1TFkaCyN8Y0fKTO2KX5iY1%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;477&quot; height=&quot;390&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;1260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 실제 학습은 그 이후부터 시작된다고 볼 수 있습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;988&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtcTOX/dJMcafNxlpf/HeSjHoaLXDxYYHvA6dJXIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtcTOX/dJMcafNxlpf/HeSjHoaLXDxYYHvA6dJXIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtcTOX/dJMcafNxlpf/HeSjHoaLXDxYYHvA6dJXIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtcTOX%2FdJMcafNxlpf%2FHeSjHoaLXDxYYHvA6dJXIk%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;543&quot; height=&quot;308&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;988&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 뉴럴 네트워크는 숫자로 변환된 데이터를 어떤 방식으로 학습하게 될까요?&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;임베딩까지 마쳤습니다. 위 이미지를 다시 확인해보면, 실제 학습은 은닉층(Hidden Layer)에서 진행되는 것을 알 수 있습니다.&lt;br&gt;그렇다면 은닉층에서는 어떤 일이 발생하게 될까요?&lt;br&gt;쉽게 말하면 은닉층은 임베딩을 통해 구한 좌표값에 가중치(Weight)를 부여하여 데이터의 중요도를 학습하는 과정이라고 볼 수 있습니다. 은닉층에서 말하는 가중치는 어떤 데이터를 얼마나 중요하게 볼 것인가에 있습니다.&lt;br&gt;예를 들어 &quot;사, 랑, 하&quot;라는 입력이 들어왔다고 가정해보겠습니다. 컴퓨터는 어떤 글자가 중요한지, 어떤 패턴이 자주 등장하는지, 어떤 연결이 강한지를 숫자로 조정하며 학습하게 됩니다.&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;뉴럴 네트워크는 예측한 값과 실제 정답을 비교하여 가중치를 조정하게 됩니다. 예를 들어 &quot;사, 랑, 하&quot; 다음에 &quot;다&quot;가 정답이라고 가정하였는데 다른 글자를 높은 확률로 예측하였다면, 오차(loss)가 크게 발생하게 됩니다.&lt;br&gt;이후 뉴럴 네트워크는 이 오차를 줄이기 위해 가중치를 반복적으로 수정하게 됩니다. 즉, 학습이란 결국 정답에 가까워지도록 가중치를 계속 조정하는 과정이라고 볼 수 있습니다.&lt;br&gt;뉴럴 네트워크는 입력된 데이터를 &lt;u&gt;가중치&lt;/u&gt;와 &lt;u&gt;활성화 함수&lt;/u&gt;(Activation Function)를 통해 계산한 뒤 결과를 출력하게 됩니다. 이후 예측값과 실제 정답을 비교하여 오차(loss)를 계산하고, &lt;u&gt;역전파&lt;/u&gt;(Backpropagation)를 통해 가중치를 수정하며 학습하게 됩니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;활성화 함수&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;활성화 함수(Activation Function)는 신경망에 비선형성을 추가해주는 역할을 합니다. 만약 활성화 함수가 존재하지 않는다면 단순한 선형 계산만 반복되기 때문에 복잡한 패턴을 학습하기 어려워집니다.&lt;br&gt;대표적으로 &lt;u&gt;sigmoid, relu, tanh&lt;/u&gt;와 같은 활성화 함수들이 존재합니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;1260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d13lkV/dJMb997zpEv/BgXQskX6kd1ZCTxhVWJwpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d13lkV/dJMb997zpEv/BgXQskX6kd1ZCTxhVWJwpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d13lkV/dJMb997zpEv/BgXQskX6kd1ZCTxhVWJwpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd13lkV%2FdJMb997zpEv%2FBgXQskX6kd1ZCTxhVWJwpk%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;422&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;1260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그렇다면 왜 단순한 선형 계산만으로는 복잡한 패턴을 학습하기 어려운 것일까요? &lt;/b&gt;&lt;br&gt;&lt;br&gt;가장 큰 이유는 선형 계산은 결국 직선 형태의 결과만 표현할 수 있기 때문입니다. 예를 들어 활성화 함수가 다음과 같다고 가정해보겠습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vwvf2/dJMcaayFvHo/qJvBzovwgk5fJTefaG2a80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vwvf2/dJMcaayFvHo/qJvBzovwgk5fJTefaG2a80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vwvf2/dJMcaayFvHo/qJvBzovwgk5fJTefaG2a80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvwvf2%2FdJMcaayFvHo%2FqJvBzovwgk5fJTefaG2a80%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;625&quot; height=&quot;382&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 의문은 어째서 단순한 선형 계산만 반복이 되는게 왜 복잡한 패턴을 학습하기 어려울까요?&lt;/b&gt;&lt;br&gt;가장 큰 이유는 선형 계산은 결국 직선 형태의 결과만 표현할 수 있기 때문입니다. 예를 들어 활성화 함수가 다음과 같다고 가정해보겠습니다.&amp;nbsp; 그리고 입력값이 -1, 2, 3이라고 가정해보겠습니다. 이렇게 되면 모든 데이터는 결국 하나의 직선 위에 존재하게 됩니다. &lt;br&gt;하지만 실제 데이터는 단순한 직선 형태로만 존재하지 않습니다. 이미지, 언어, 음성 데이터처럼 복잡한 데이터들은 곡선 형태의 관계나 비선형적인 패턴을 가지는 경우가 많습니다. &lt;br&gt;&lt;br&gt;따라서 뉴럴 네트워크는 활성화 함수를 통해 비선형성을 추가하여 복잡한 패턴 역시 학습할 수 있도록 구성됩니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HKoRH/dJMcajoJfS8/rvfdSya1QUFmBlaJKin9C0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HKoRH/dJMcajoJfS8/rvfdSya1QUFmBlaJKin9C0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HKoRH/dJMcajoJfS8/rvfdSya1QUFmBlaJKin9C0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHKoRH%2FdJMcajoJfS8%2FrvfdSya1QUFmBlaJKin9C0%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;547&quot; height=&quot;395&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;역전파&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;뉴럴 네트워크는 예측값과 실제 정답을 비교하여 오차(loss)를 계산한다고 하였습니다. 이러한 과정을 통해 어떤 부분이 잘못 예측되었는지를 확인하고, 이를 기반으로 학습을 진행하게 됩니다.&lt;br&gt;오차를 보고 수정한다는 개념까지는 이해할 수 있습니다. 하지만 여기서 몇 가지 문제가 발생합니다.&lt;br&gt;신경망에는 수많은 가중치(Weight)가 존재합니다. 그렇다면 어떤 가중치가 잘못된 것일까요?&lt;br&gt;또한 어느 정도 수정해야 하는지 역시 알 수 없습니다. 이러한 문제를 해결하기 위해 등장한 개념이 역전파(Backpropagation)입니다.&lt;br&gt;쉽게 말하면 역전파는 &quot;책임 분배&quot; 과정이라고 볼 수 있습니다. 출력된 오차를 기반으로 원인을 출력층(Output Layer)에서 입력층(Input Layer) 방향으로 거꾸로 추적하면서, 각 가중치에게 얼마나 책임이 있는지를 계산하게 됩니다.&lt;br&gt;역전파라는 이름 역시 여기서 등장합니다. 계산은 입력 → 출력 방향으로 진행되지만, 오차의 책임은 출력 → 입력 방향으로 전달되기 때문입니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9hhVx/dJMcafmr0Wp/eY7gBHFWj3MpPY6k81vGz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9hhVx/dJMcafmr0Wp/eY7gBHFWj3MpPY6k81vGz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9hhVx/dJMcafmr0Wp/eY7gBHFWj3MpPY6k81vGz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9hhVx%2FdJMcafmr0Wp%2FeY7gBHFWj3MpPY6k81vGz0%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;725&quot; height=&quot;388&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;/figure&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;입력값: x = 2&lt;/li&gt;&lt;li&gt;가중치: w1 = 0.5, w2 = 3&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;은닉값(h)은 다음과 같이 계산됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;h=w1*x=0.5×2=1&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;출력값(y)은 다음과 같습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;y=w2*h=3×1=3&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 정답이 9라고 가정해보겠습니다.&lt;br&gt;그렇다면 오차(loss)는 다음과 같이 계산됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;(9−3)^2=36&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 현재 예측값과 실제 정답의 차이가 크기 때문에 loss 역시 크게 측정된다는 뜻입니다.&lt;br&gt;이제 역전파는 이 오차를 줄이기 위해 어떤 가중치가 얼마나 잘못되었는지를 계산하기 시작합니다.&lt;br&gt;마지막으로 softmax와 cross-entropy를 간단하게 알아보고 마치겠습니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;softmax&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;출력층(Output Layer)에서는 여러 값들이 계산되어 출력됩니다. 하지만 이 값들은 아직 확률이라고 보기 어렵습니다.&lt;br&gt;예를 들어 다음과 같은 값이 출력되었다고 가정해보겠습니다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;다 → 3.2&lt;/li&gt;&lt;li&gt;기 → 1.1&lt;/li&gt;&lt;li&gt;늙 → 0.3&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서는 어떤 값이 얼마나 높은 확률을 가지는지 직관적으로 이해하기 어렵습니다.&lt;br&gt;이러한 값을 확률 형태로 변환해주는 함수가 Softmax입니다.&lt;br&gt;Softmax는 출력값들을 &lt;u&gt;0~1 사이의 값으로 변환&lt;/u&gt;하며, 전체 합이 1이 되도록 만들어줍니다.&lt;br&gt;예를 들어, 다음과 같이 변환될 수 있습니다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;다 → 0.83&lt;/li&gt;&lt;li&gt;기 → 0.12&lt;/li&gt;&lt;li&gt;늙 → 0.05&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 뉴럴 네트워크는 &quot;다&quot;가 가장 높은 확률을 가진다고 판단하게 됩니다.&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;cross-entropy&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Cross Entropy(교차 엔트로피)는 뉴럴 네트워크가 예측한 확률과 &lt;u&gt;실제 정답이 얼마나 차이가 나는지를 계산하는 Loss 함수&lt;/u&gt;입니다.&lt;br&gt;예를 들어 다음과 같은 결과가 나왔다고 가정해보겠습니다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;다 → 0.83&lt;/li&gt;&lt;li&gt;기 → 0.12&lt;/li&gt;&lt;li&gt;늙 → 0.05&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 실제 정답이 &quot;다&quot;라고 해보겠습니다.&lt;br&gt;이 경우 뉴럴 네트워크는 정답인 &quot;다&quot;를 높은 확률로 예측하였기 때문에 Loss 값은 낮게 측정됩니다.&lt;br&gt;반대로 다음과 같은 결과가 나왔다고 가정해보겠습니다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;다 → 0.10&lt;/li&gt;&lt;li&gt;기 → 0.80&lt;/li&gt;&lt;li&gt;늙 → 0.10&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 실제 정답인 &quot;다&quot;를 낮은 확률로 예측하였기 때문에 Loss 값은 크게 증가하게 됩니다.&lt;br&gt;즉, Cross Entropy는 정답에 가까운 확률을 예측할수록 Loss를 낮게 만들고, 정답과 멀어질수록 Loss를 크게 증가시키는 역할을 합니다.&lt;br&gt;보통 Softmax와 함께 사용되며, 뉴럴 네트워크는 Cross Entropy를 최소화하는 방향으로 가중치를 수정하며 학습을 진행하게 됩니다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;마지막으로 전체 흐름을 한번에 확인해보겠습니다.&lt;/h2&gt;&lt;div style=&quot;background-color: #191a1b; color: #bfbfbf; text-align: start;&quot;&gt; 
 &lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;입력 문장: &quot;나는 강아지를 ___&quot;
   ↓ ① 토큰화
[나는=42, 강아지를=1057, ___]
   ↓ ② 임베딩 조회
[0.2, 0.5, ...] [0.1, 0.9, ...]   ← 각 토큰의 벡터
   ↓ ③ 합치기 (concatenate)
[0.2, 0.5, ..., 0.1, 0.9, ...]    ← 하나의 큰 벡터
   ↓ ④ 은닉층 (행렬곱 + 활성화)
[0.7, -0.3, 0.2, ...]              ← 압축된 의미
   ↓ ⑤ 출력층 (어휘수만큼 점수)
[3.2, -1.5, 0.8, 7.1, ..., 0.4]   ← 각 단어의 점수
   ↓ ⑥ Softmax
[1%, 0.1%, 5%, 70%, ..., 2%]      ← 확률
   ↓ ⑦ 정답과 비교 (Cross-Entropy)
Loss = 1.99
   ↓ ⑧ 역전파
모든 가중치 + 임베딩까지 살짝 조정&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 뉴럴 네트워크에 대해 알아보았습니다. 이번 글에서는 뉴럴 네트워크가 어떤 방식으로 동작하는지에 초점을 맞춰 설명하였습니다.&lt;br&gt;그러다 보니 뉴럴 네트워크가 새로운 문장을 어떻게 만들어내는지에 대한 설명은 다소 부족하다고 느껴졌습니다. 물론, 확률을 기반으로 loss를 계산하고, 이를 통해 어떤 값이 더 정답에 가까운지를 학습한다는 흐름까지는 이해할 수 있었습니다.&lt;br&gt;&lt;br&gt;그래서 다음 글에서는 뉴럴 네트워크가 어떤 방식으로 새로운 문장을 생성하는지에 대해 조금 더 자세히 알아보려고 합니다.&lt;br&gt;또한 이번 글에서 설명하지 못한 부분이 하나 있습니다. 앞에서 임베딩은 단어를 좌표 형태로 변환한다고 설명하였습니다. 하지만 실제로는 각 단어를 표현하기 위한 좌표값이 하나만 존재하는 것이 아닙니다. 상황에 따라 수십 개, 수백 개 이상의 좌표값을 가질 수도 있습니다. 이러한 좌표의 개수를 임베딩 차원(Dimension)이라고 부릅니다.&lt;br&gt;일반적으로 차원이 커질수록 더 많은 정보를 표현할 수 있기 때문에 정확도는 높아질 수 있습니다. 하지만 반대로 계산량 증가, 과적합, 희소성 문제와 같은 부작용 역시 함께 발생할 수 있다는 점도 고려해야 합니다.&lt;/p&gt;</description>
      <category>개발/LLM</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/581</guid>
      <comments>https://b-programmer.tistory.com/581#entry581comment</comments>
      <pubDate>Mon, 25 May 2026 15:52:42 +0900</pubDate>
    </item>
    <item>
      <title>외부 API 호출은 어떤 라이브러리를 선택해야 할까</title>
      <link>https://b-programmer.tistory.com/580</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API에 접근하기 위해서는 HTTP 통신이 필요합니다. 직접 HTTP 통신 코드를 구현할 수도 있지만, Spring 환경에서는 WebClient, RestTemplate, FeignClient와 같은 HTTP 클라이언트 라이브러리를 사용할 수 있습니다. 그렇다면 어떤 라이브러리를 사용하는 것이 좋을까요?&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-end=&quot;695&quot; data-start=&quot;508&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 팀 전체가 RestTemplate에 익숙한 환경이라면, 무조건 최신 기술이라는 이유만으로 WebClient를 도입하는 것이 오히려 유지보수 비용을 증가시킬 수 있습니다. 반대로 외부 API 호출이 많고 timeout, retry, logging과 같은 기능이 중요하다면 WebClient가 더 적합할 수 있습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;695&quot; data-start=&quot;508&quot; data-ke-size=&quot;size26&quot;&gt;그럼에도 불구하고 webClient를 선택하였습니다. 그 이유는?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 간단하게 비교를 진행해봅시다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RestTemplate&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vFjTU/dJMcaicndQ4/ePZkqiqg0qnKo5ICoPYoSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vFjTU/dJMcaicndQ4/ePZkqiqg0qnKo5ICoPYoSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vFjTU/dJMcaicndQ4/ePZkqiqg0qnKo5ICoPYoSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvFjTU%2FdJMcaicndQ4%2FePZkqiqg0qnKo5ICoPYoSk%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;1582&quot; height=&quot;248&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div data-is-intersecting=&quot;true&quot; data-turn-id-container=&quot;request-6a0d8edc-4dec-83a4-a567-54dea1484f3b-8&quot;&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5-thinking&quot; data-message-id=&quot;9dd8e263-99d2-4d47-9e56-5d36385417c0&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-end=&quot;164&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;가장 큰 장점은 구축하기 쉽다는 점입니다. RestTemplate은 동기 방식 기반으로 동작하기 때문에 코드 흐름이 직관적이고, 요청과 응답의 흐름을 이해하기 쉽습니다. 또한 기존 프로젝트에서도 많이 사용되어 왔기 때문에 레퍼런스가 많고, 러닝 커브가 낮다는 점 역시 장점이라고 생각합니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;423&quot; data-start=&quot;166&quot; data-ke-size=&quot;size16&quot;&gt;하지만 기본적으로 동기 방식만 지원하기 때문에 비동기 처리를 하고 싶다면 @Async, CompletableFuture, ExecutorService와 같은 자바 비동기 기능을 추가로 구성해야 합니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfBbBu/dJMcahYNl8Y/hNjfnzQPiMYtZ5gTbpY61k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfBbBu/dJMcahYNl8Y/hNjfnzQPiMYtZ5gTbpY61k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfBbBu/dJMcahYNl8Y/hNjfnzQPiMYtZ5gTbpY61k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfBbBu%2FdJMcahYNl8Y%2FhNjfnzQPiMYtZ5gTbpY61k%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;776&quot; height=&quot;223&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;423&quot; data-start=&quot;166&quot; data-ke-size=&quot;size16&quot;&gt;즉, 비동기 처리를 자연스럽게 지원한다기보다는 별도의 구조를 덧붙여야 하는 방식에 가깝습니다. 그렇기 때문에 외부 API 호출이 많아지고, timeout, retry, logging 같은 세밀한 제어가 필요해질수록 다소 불편함이 생길 수 있습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;423&quot; data-start=&quot;166&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, RestTemplate에서 요청/응답 로깅이나 공통 처리를 하려면 보통 ClientHttpRequestInterceptor를 사용합니다.&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z9T0Q/dJMcahEs46a/KecC23iakNyywtkdA9cXMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z9T0Q/dJMcahEs46a/KecC23iakNyywtkdA9cXMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z9T0Q/dJMcahEs46a/KecC23iakNyywtkdA9cXMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz9T0Q%2FdJMcahEs46a%2FKecC23iakNyywtkdA9cXMK%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;353&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;742&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;970&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baVKZ1/dJMcacpGoSh/6W7JAagAzgZvM6bGFe2dKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baVKZ1/dJMcacpGoSh/6W7JAagAzgZvM6bGFe2dKK/img.png&quot; data-alt=&quot;config에서 등록&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baVKZ1/dJMcacpGoSh/6W7JAagAzgZvM6bGFe2dKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaVKZ1%2FdJMcacpGoSh%2F6W7JAagAzgZvM6bGFe2dKK%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;970&quot; height=&quot;146&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;config에서 등록&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 공통 logging이나 header 처리를 위해서는 ClientHttpRequestInterceptor를 직접 구성해야 합니다. 물론 확장은 가능하지만, 외부 API 호출이 많아질수록 설정 코드 역시 점점 증가할 가능성이 높습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FeignClient&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Feign은 어떨까요? 편의상 Client는 빼고 Feign이라고 부르겠습니다. Feign은 선언형 방식으로 동작합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhnIry/dJMcaaFqfu1/xhd5rxWEINtgXkeZXgaao0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhnIry/dJMcaaFqfu1/xhd5rxWEINtgXkeZXgaao0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhnIry/dJMcaaFqfu1/xhd5rxWEINtgXkeZXgaao0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhnIry%2FdJMcaaFqfu1%2Fxhd5rxWEINtgXkeZXgaao0%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;386&quot; height=&quot;152&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 선언형이란 무엇일까요? 위에서 살펴본 RestTemplate은 필요한 위치에서 직접 객체를 주입받아 메서드를 호출하는 방식입니다. 물론 스프링 빈으로 등록해서 공통으로 사용할 수 있지만, API 호출 코드가 서비스 로직 안에 섞이기 쉽다는 부담이 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;523&quot; data-start=&quot;352&quot; data-ke-size=&quot;size16&quot;&gt;반면, Feign은 인터페이스 기반으로 외부 API 호출을 정의합니다. 즉, 어떤 URL로 요청을 보낼지, 어떤 메서드를 사용할지, 어떤 값을 파라미터로 전달할지를 인터페이스에 선언해두고 사용할 수 있습니다. 이런 특징 덕분에 외부 API 호출 규격을 한곳에 모아 관리하기 쉽고, 코드도 비교적 간결해집니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;608&quot; data-start=&quot;525&quot; data-ke-size=&quot;size16&quot;&gt;또한 서비스 간 호출 구조가 많은 MSA 환경에서는 각 서비스의 API 호출을 Feign 인터페이스로 분리해 관리할 수 있기 때문에 자주 사용됩니다.&lt;/p&gt;
&lt;h4 data-end=&quot;608&quot; data-start=&quot;525&quot; data-ke-size=&quot;size20&quot;&gt;그렇다면 비동기는 어떻게 지원할까요?&lt;/h4&gt;
&lt;div&gt;
&lt;div data-is-intersecting=&quot;true&quot; data-turn-id-container=&quot;request-6a0d8edc-4dec-83a4-a567-54dea1484f3b-12&quot;&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5&quot; data-message-id=&quot;36b60125-42cc-4b5a-99dc-ef107d17b97d&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-end=&quot;144&quot; data-start=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;아쉽게도 Feign 또한 기본적으로 동기 방식으로 동작합니다. 그렇기 때문에 RestTemplate와 비슷하게 직관적으로 사용할 수 있다는 장점이 존재합니다.&lt;/p&gt;
&lt;p data-end=&quot;291&quot; data-start=&quot;146&quot; data-ke-size=&quot;size16&quot;&gt;물론 비동기 방식으로 사용하는 방법도 존재합니다. CompletableFuture와 같은 자바 비동기 기능을 함께 사용할 수도 있고, 비동기를 지원하는 별도의 설정을 추가할 수도 있습니다. 하지만 기본적으로는 동기 호출에 가까운 방식이라고 볼 수 있습니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tJnf2/dJMcajhZGQl/bZHknIC5xoSVuhBCUffkD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tJnf2/dJMcajhZGQl/bZHknIC5xoSVuhBCUffkD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tJnf2/dJMcajhZGQl/bZHknIC5xoSVuhBCUffkD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtJnf2%2FdJMcajhZGQl%2FbZHknIC5xoSVuhBCUffkD1%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;1076&quot; height=&quot;294&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;294&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-start=&quot;525&quot; data-end=&quot;608&quot;&gt;얼핏봤을때 RestTemplate와 비슷하게 보이는데 선언형으로 만들었을때 무슨 장점이 있나?&lt;/h4&gt;
&lt;div&gt;
&lt;p data-end=&quot;280&quot; data-start=&quot;210&quot; data-ke-size=&quot;size16&quot;&gt;하지만 선언형으로 구성했을때 가장 큰 차이는 외부 API 호출 자체를 하나의 역할로 분리할 수 있다는 점에 있다고 생각합니다.&lt;/p&gt;
&lt;p data-end=&quot;355&quot; data-start=&quot;282&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 RestTemplate는 아래처럼 서비스 내부에서 직접 URL, header, 요청 방식을 작성하는 경우가 많습니다.&lt;/p&gt;
&lt;p data-end=&quot;442&quot; data-start=&quot;357&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 간단하지만, 외부 API 호출 코드가 서비스 로직 안으로 들어오게 됩니다. 즉, 비즈니스 로직과 HTTP 호출 로직이 서로 섞이기 쉬워집니다.&lt;/p&gt;
&lt;p data-end=&quot;478&quot; data-start=&quot;444&quot; data-ke-size=&quot;size16&quot;&gt;반면 Feign은 외부 API 자체를 인터페이스로 분리합니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyN33L/dJMcagMo7Qh/0XQAKBv3Q7XUU7TUDAHksk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyN33L/dJMcagMo7Qh/0XQAKBv3Q7XUU7TUDAHksk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyN33L/dJMcagMo7Qh/0XQAKBv3Q7XUU7TUDAHksk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyN33L%2FdJMcagMo7Qh%2F0XQAKBv3Q7XUU7TUDAHksk%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;803&quot; height=&quot;284&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구성하면 서비스에서는 외부 API 호출 방법을 신경쓰기보다는, 그냥 메서드를 호출하듯 사용할 수 있습니다. 즉, 선언형 방식의 장점은 단순히 코드가 짧아지는 것이 아니라 외부 API 역할 분리, 유지보수성 증가, 중복 감소,API 호출 규격 관리 용이,테스트 구조 분리 쉬움과 같은 구조적인 장점에 있다고 생각합니다. 특히 MSA처럼 서비스 간 호출이 많아질수록 &amp;ldquo;HTTP 요청 코드&amp;rdquo;를 여기저기 작성하는 것보다, 인터페이스 단위로 관리하는 방식이 훨씬 깔끔해질 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebClient&lt;/h3&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;954&quot; data-start=&quot;853&quot; data-ke-size=&quot;size16&quot;&gt;이제 마지막으로 WebClient에 대해 이야기해봅시다. WebClient는 Spring WebFlux에서 제공하는 HTTP Client입니다. 기존 RestTemplate와 다르게 비동기 및 논블로킹 방식을 지원한다는 특징이 존재합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwDRiZ/dJMcaa6tO9P/4KjNMQQOg1v0maHTQspLqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwDRiZ/dJMcaa6tO9P/4KjNMQQOg1v0maHTQspLqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwDRiZ/dJMcaa6tO9P/4KjNMQQOg1v0maHTQspLqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwDRiZ%2FdJMcaa6tO9P%2F4KjNMQQOg1v0maHTQspLqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;175&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-end=&quot;400&quot; data-start=&quot;383&quot; data-ke-size=&quot;size20&quot;&gt;그렇다면 논블로킹은 무엇일까요?&lt;/h4&gt;
&lt;p data-end=&quot;496&quot; data-start=&quot;402&quot; data-ke-size=&quot;size16&quot;&gt;기존 RestTemplate는 외부 API 요청을 보내면 응답이 올때까지 현재 스레드가 대기하게 됩니다. 즉, 응답이 돌아오기 전까지 다른 작업을 처리하지 못합니다.&lt;/p&gt;
&lt;p data-end=&quot;500&quot; data-start=&quot;395&quot; data-ke-size=&quot;size16&quot;&gt;반면 WebClient는 요청을 보낸 이후 응답을 기다리는 동안 스레드를 계속 점유하지 않습니다. 즉, 다른 작업을 수행하다가 응답이 도착했을때 다시 이어서 처리하는 방식에 가깝습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxB5qC/dJMcaak3J8m/cdefkoCBoiVrAQCgoEhtD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxB5qC/dJMcaak3J8m/cdefkoCBoiVrAQCgoEhtD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxB5qC/dJMcaak3J8m/cdefkoCBoiVrAQCgoEhtD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxB5qC%2FdJMcaak3J8m%2FcdefkoCBoiVrAQCgoEhtD0%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;1082&quot; height=&quot;234&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div data-is-intersecting=&quot;true&quot; data-turn-id-container=&quot;request-6a0d8edc-4dec-83a4-a567-54dea1484f3b-21&quot;&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5&quot; data-message-id=&quot;c012f6ce-30a6-445a-bc69-b68531e346e3&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-end=&quot;563&quot; data-start=&quot;502&quot; data-ke-size=&quot;size16&quot;&gt;이러한 특징 때문에 외부 API 호출이 많거나, 동시에 많은 요청을 처리해야 하는 환경에서 장점이 존재합니다.&lt;/p&gt;
&lt;p data-end=&quot;636&quot; data-start=&quot;565&quot; data-ke-size=&quot;size16&quot;&gt;또한 WebClient는 단순히 비동기만 지원하는 것이 아니라, 외부 API 호출 흐름 자체를 세밀하게 제어할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;722&quot; data-start=&quot;638&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 timeout, retry, logging, 공통 header, 응답 상태별 예외 처리와 같은 기능들을 비교적 유연하게 구성할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;877&quot; data-start=&quot;724&quot; data-ke-size=&quot;size16&quot;&gt;물론, RestTemplate 역시 interceptor나 config를 이용해 공통 설정을 구성할 수 있습니다. 하지만 timeout, logging, header 처리와 같은 기능들이 점점 늘어나게 되면 설정 코드가 커지고, 요청 흐름을 파악하기 어려워질 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;964&quot; data-start=&quot;879&quot; data-ke-size=&quot;size16&quot;&gt;반면, WebClient는 builder와 filter 기반으로 흐름을 구성할 수 있기 때문에 요청 처리 과정을 비교적 명확하게 관리할 수 있었습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1069&quot; data-start=&quot;966&quot; data-ke-size=&quot;size16&quot;&gt;즉, WebClient의 핵심은 단순히 비동기를 지원한다는 것이 아니라, 외부 API 호출 과정에서 발생할 수 있는 다양한 흐름을 유연하게 제어할 수 있다는 점에 있다고 생각합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1069&quot; data-start=&quot;966&quot; data-ke-size=&quot;size23&quot;&gt;RestClient&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring6부터 등장한 새로운 HTTP Client입니다. 기존 RestTemplate의 단순하고 직관적인 사용 방식은 유지하면서, 조금 더 현대 적인 API 스타일로 개선되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 RestTemplate는 다음과 같이 작성하였습니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;325&quot; data-origin-height=&quot;37&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxFXec/dJMcada0O7l/qaPUqPLktY4VvdOw9vSQoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxFXec/dJMcada0O7l/qaPUqPLktY4VvdOw9vSQoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxFXec/dJMcada0O7l/qaPUqPLktY4VvdOw9vSQoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxFXec%2FdJMcada0O7l%2FqaPUqPLktY4VvdOw9vSQoK%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;430&quot; height=&quot;37&quot; data-origin-width=&quot;325&quot; data-origin-height=&quot;37&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;반면, RestClient는 builder 기반으로 조금 더 직관적으로 구성할 수 있습니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;436&quot; data-origin-height=&quot;137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GP79Y/dJMcai4sT8k/flqrgu9den9sXmSndTj93K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GP79Y/dJMcai4sT8k/flqrgu9den9sXmSndTj93K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GP79Y/dJMcai4sT8k/flqrgu9den9sXmSndTj93K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGP79Y%2FdJMcai4sT8k%2Fflqrgu9den9sXmSndTj93K%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;659&quot; height=&quot;207&quot; data-origin-width=&quot;436&quot; data-origin-height=&quot;137&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 data-end=&quot;1069&quot; data-start=&quot;966&quot; data-ke-size=&quot;size23&quot;&gt;그래서 RestClient를 선택한 이유가 무엇인가요?&lt;/h3&gt;
&lt;p data-end=&quot;451&quot; data-start=&quot;373&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 RestTemplate, FeignClient, WebClient, RestClient에 대해 간단하게 알아봤습니다.&lt;/p&gt;
&lt;p data-end=&quot;661&quot; data-start=&quot;453&quot; data-ke-size=&quot;size16&quot;&gt;가장 러닝 커브가 낮은 것은 RestTemplate, 가장 선언적으로 사용하기 좋은 것은 FeignClient, 그리고 비동기 및 논블로킹 방식을 지원하는 것은 WebClient입니다. 그리고 RestClient는 기존 RestTemplate의 직관적인 사용 방식과 WebClient의 현대적인 API 스타일을 함께 가져온 기술이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;693&quot; data-start=&quot;663&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 어떤 기술을 도입하는 것이 가장 현실적일까요?&lt;/p&gt;
&lt;p data-end=&quot;805&quot; data-start=&quot;695&quot; data-ke-size=&quot;size16&quot;&gt;현재는 AI의 등장으로 인해 과거보다 러닝 커브에 대한 부담이 많이 낮아졌다고 생각합니다. 하지만 현재 프로젝트에서는 Reactive 기반의 비동기 흐름까지 사용할 필요는 크지 않다고 판단했습니다.&lt;/p&gt;
&lt;p data-end=&quot;942&quot; data-start=&quot;807&quot; data-ke-size=&quot;size16&quot;&gt;물론 WebClient 역시 Spring MVC 환경에서 사용할 수 있습니다. 하지만 Mono, Flux와 같은 Reactive 흐름을 적극적으로 사용하지 않는다면, 현재 프로젝트에서는 오히려 구조가 과해질 수 있다고 느껴졌습니다.&lt;/p&gt;
&lt;p data-end=&quot;1060&quot; data-start=&quot;944&quot; data-ke-size=&quot;size16&quot;&gt;반면 RestClient는 기존 MVC 환경에서도 비교적 자연스럽게 사용할 수 있었고, RestTemplate보다 조금 더 현대적인 방식으로 HTTP 요청 흐름을 구성할 수 있다는 장점이 존재했습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1106&quot; data-start=&quot;1062&quot; data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 현재 프로젝트에서는 RestClient를 선택하게 되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p data-end=&quot;120&quot; data-start=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;AI의 발전으로 인해 과거보다 훨씬 적극적으로 새로운 기술을 선택할 수 있는 환경이 되었다고 생각합니다. 예전에는 러닝 커브나 레퍼런스 부족 때문에 익숙한 기술을 우선적으로 선택하는 경우가 많았습니다.&lt;/p&gt;
&lt;p data-end=&quot;218&quot; data-start=&quot;122&quot; data-ke-size=&quot;size16&quot;&gt;만약 3년 전에 동일한 프로젝트를 진행했다면, 저 역시 RestClient보다는 기존에 많이 사용해왔던 RestTemplate를 선택했을 가능성이 높다고 생각합니다.&lt;/p&gt;
&lt;p data-end=&quot;409&quot; data-start=&quot;220&quot; data-ke-size=&quot;size16&quot;&gt;물론 RestTemplate를 사용하는 것이 잘못되었다는 의미는 아닙니다. 실제로 지금도 많은 프로젝트에서 안정적으로 사용되고 있는 기술입니다. 다만 현재 프로젝트에서는 Reactive 기반까지 사용할 필요는 크지 않았지만, 기존 RestTemplate보다는 조금 더 현대적인 방식으로 외부 API 호출 흐름을 관리하고 싶었습니다.&lt;/p&gt;
&lt;p data-end=&quot;534&quot; data-start=&quot;411&quot; data-ke-size=&quot;size16&quot;&gt;그 과정에서 RestClient는 RestTemplate의 직관적인 사용 방식은 유지하면서도, builder 기반의 현대적인 API 스타일을 제공한다는 점에서 현재 프로젝트와 가장 잘 맞는 선택이라고 느껴졌습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;611&quot; data-start=&quot;536&quot; data-ke-size=&quot;size16&quot;&gt;결국 중요한 것은 최신 기술을 사용하는 것이 아니라, 현재 프로젝트의 목표와 환경에 가장 잘 맞는 기술을 선택하는 것이라고 생각합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>개발/이력관리</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/580</guid>
      <comments>https://b-programmer.tistory.com/580#entry580comment</comments>
      <pubDate>Thu, 21 May 2026 23:43:13 +0900</pubDate>
    </item>
    <item>
      <title>N-gram을 이용해서 새로운 데이터 만들기</title>
      <link>https://b-programmer.tistory.com/579</link>
      <description>&lt;figure id=&quot;og_1778946111161&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;N-gram 이해해보기&quot; data-og-description=&quot;TL;DR; N그램을 설명한 글입니다. LLM from Scratch - Part 1. Statistical Language Models[LLM 바닥부터 만들기 - 파트1. 통계적 언어 모델의 원리] 부담 없이 즐기며 배우는 LLM 입문 강의  오픈이벤트 50%할인! (~5&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/577&quot; data-og-url=&quot;https://b-programmer.tistory.com/577&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/HCroH/dJMb82MInfD/MEt6fWVKUqAokGpc9G39R1/img.png?width=266&amp;amp;height=190&amp;amp;face=0_0_266_190,https://scrap.kakaocdn.net/dn/5hgn4/dJMb8YXQRUo/39HjHtKhjYeWUIIsHFDhU1/img.png?width=266&amp;amp;height=190&amp;amp;face=0_0_266_190,https://scrap.kakaocdn.net/dn/cgb0xf/dJMb8SXC5y6/IWOogTdeGpM9pZw7GbL5k0/img.png?width=616&amp;amp;height=770&amp;amp;face=0_0_616_770&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/577&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/577&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/HCroH/dJMb82MInfD/MEt6fWVKUqAokGpc9G39R1/img.png?width=266&amp;amp;height=190&amp;amp;face=0_0_266_190,https://scrap.kakaocdn.net/dn/5hgn4/dJMb8YXQRUo/39HjHtKhjYeWUIIsHFDhU1/img.png?width=266&amp;amp;height=190&amp;amp;face=0_0_266_190,https://scrap.kakaocdn.net/dn/cgb0xf/dJMb8SXC5y6/IWOogTdeGpM9pZw7GbL5k0/img.png?width=616&amp;amp;height=770&amp;amp;face=0_0_616_770');&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;N-gram 이해해보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;TL;DR; N그램을 설명한 글입니다. LLM from Scratch - Part 1. Statistical Language Models[LLM 바닥부터 만들기 - 파트1. 통계적 언어 모델의 원리] 부담 없이 즐기며 배우는 LLM 입문 강의  오픈이벤트 50%할인! (~5&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 국립국어사전 데이터를 기반으로 N-gram을 적용한 적이 있습니다. 당시에는 총 548,385개의 단어를 추출하였으며, 한 글자 단어, 속담, 관용구, 구 형태의 표현, 신조어 등은 제외하였습니다. N-gram을 간단히 설명하자면, 이전 데이터(문맥)를 기반으로 다음에 어떤 데이터가 등장할지를 예측하는 방식입니다. 하지만 당시 실험을 진행하면서 한 가지 아쉬운 점이 있었습니다. 사전 중심으로 데이터를 구성하다 보니, 실제 LLM이 동작하는 방식과는 다소 거리가 있다고 느껴졌기 때문입니다. 실제 LLM은 단순 사전 데이터가 아니라, 문서와 문장 단위의 대규모 텍스트 데이터를 기반으로 학습이 이루어집니다. 그래서 이번에는 위키피디아와 같이 외부에 공개되어 있는 실제 문서 데이터를 기반으로 N-gram을 구성해보려고 합니다. 과연 실제 문서를 기반으로 새로운 문장을 생성했을 때, 얼마나 자연스럽게 동작할 수 있을까요? 그리고 어느 정도까지 문맥을 이해하는 것처럼 보일 수 있을까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;760&quot; data-start=&quot;740&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 이를 직접 확인해보겠습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;760&quot; data-start=&quot;740&quot; data-ke-size=&quot;size26&quot;&gt;위키 문서에서 문서를 추출해봅시다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 학습에는 꽤 오랜 시간이 필요했습니다. 전체적으로 약 2~3시간 정도 문서를 수집하고 정리하는 작업이 진행된 것 같습니다. 문학 자료의 경우 약 1,531개를 추출하였으며, 비문학 자료는 무려 480,538개를 수집하였습니다. 데이터 출처는 주로 위키피디아와 위키문헌을 기반으로 구성하였습니다. 특히 비문학 자료는 수집된 문서 수 자체가 매우 많았기 때문에 데이터 정리와 전처리에 상당한 시간이 소요되었습니다. 반면 문학 자료의 경우에는 단순히 문서 수를 늘리는 것보다, 실제 순수 문학에 가까운 데이터만 남기기 위해 문학성이 낮거나 성격이 다른 자료들은 제외하는 방식으로 정리를 진행하였습니다.&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;690&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k20pV/dJMb990Kz5y/OiifXsbxei9QMxsCFn8Gy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k20pV/dJMb990Kz5y/OiifXsbxei9QMxsCFn8Gy1/img.png&quot; data-alt=&quot;문학 자료 일부분입니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k20pV/dJMb990Kz5y/OiifXsbxei9QMxsCFn8Gy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk20pV%2FdJMb990Kz5y%2FOiifXsbxei9QMxsCFn8Gy1%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;335&quot; height=&quot;280&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;문학 자료 일부분입니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데이터를 이용해서 uni-gram부터 N-gram까지 진행해보겠습니다. 문장이 최대한 자연스러워질때까지 진행할 거 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 시작&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;uni-gram&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uni-gram부터 테스트를 진행해보겠습니다. 다만 uni-gram은 이전 문맥을 전혀 고려하지 않기 때문에, 사실상 자주 등장한 글자들을 기반으로 랜덤한 문자열을 생성하는 것에 가깝습니다. 즉, 어떤 글자가 얼마나 자주 등장했는지를 확인할 수는 있지만, 문맥 자체를 이해하지는 못하기 때문에 생성 결과에는 큰 의미가 없었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doSYmd/dJMcaa6pNar/M3rcAnPYT949VkaByPIRK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doSYmd/dJMcaa6pNar/M3rcAnPYT949VkaByPIRK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doSYmd/dJMcaa6pNar/M3rcAnPYT949VkaByPIRK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoSYmd%2FdJMcaa6pNar%2FM3rcAnPYT949VkaByPIRK1%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;476&quot; height=&quot;440&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;542&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 테스트를 진행했을 때는 단어가 아니라 음절 단위로 계산하고 있었습니다. 하지만 음절 단위로 N-gram을 구성하게 되면, 이전에 사전 데이터를 기반으로 실험했던 방식과 큰 차이가 없다고 느껴졌습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;237&quot; data-start=&quot;118&quot; data-ke-size=&quot;size16&quot;&gt;결국 실제 문장 흐름과 문맥을 조금 더 반영하기 위해서는 음절보다 단어 단위로 접근하는 것이 더 적절하다고 판단하였습니다. 그래서 여러 전처리 과정을 거친 뒤, 최종적으로 단어 기준으로 다시 계산을 진행하였습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;이것을 토대로 랜덤 문자열을 만들어보겠습니다.&lt;/p&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;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGv3xS/dJMb997tRvK/s3Y2mSLZx5d7WwPSq8Rysk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGv3xS/dJMb997tRvK/s3Y2mSLZx5d7WwPSq8Rysk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGv3xS/dJMb997tRvK/s3Y2mSLZx5d7WwPSq8Rysk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGv3xS%2FdJMb997tRvK%2Fs3Y2mSLZx5d7WwPSq8Rysk%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;601&quot; height=&quot;283&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div data-is-intersecting=&quot;true&quot; data-turn-id-container=&quot;request-6a01aeda-e668-83e8-ba06-822664e8cc17-23&quot;&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5&quot; data-message-id=&quot;3a9db6dc-6abd-40f7-a7fb-4620125d6bb1&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-end=&quot;84&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;역시 uni-gram은 순수하게 등장 빈도만 기반으로 랜덤 문자열을 생성하기 때문에, 실제로 생성된 결과가 무슨 의미인지 거의 이해할 수 없었습니다. 멀리서 보면 문장처럼 보이기는 합니다. 하지만 실제로 가까이에서 읽어보면 문맥과 의미가 전혀 이어지지 않는다는 것을 확인할 수 있었습니다. 게다가 문장의 종료 역시 일정한 규칙 없이 등장하였습니다. 즉, 모델이 문장의 흐름을 이해하고 마무리한다기보다, 단순히 등장 확률에 따라 종료 토큰이 우연히 선택되는 수준에 가까웠던 것입니다.&lt;/p&gt;
&lt;h4 data-end=&quot;23&quot; data-start=&quot;0&quot; data-ke-size=&quot;size20&quot;&gt;그렇다면 왜 이런 현상이 발생한 것일까요?&lt;/h4&gt;
&lt;p data-end=&quot;154&quot; data-start=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 uni-gram이 앞뒤 문맥을 전혀 고려하지 않기 때문입니다. uni-gram은 단순히 각 단어가 얼마나 자주 등장했는지에 대한 빈도 정보만 알고 있을 뿐, 어떤 단어들이 서로 자연스럽게 연결되는지는 알 수 없습니다.&lt;/p&gt;
&lt;p data-end=&quot;276&quot; data-start=&quot;156&quot; data-ke-size=&quot;size16&quot;&gt;즉, 단어를 하나씩 무작위로 선택하게 되면 겉보기에는 한국어 단어들이기 때문에 문장처럼 보일 수는 있습니다. 하지만 실제로는 단어 사이의 관계가 존재하지 않기 때문에 의미를 이해할 수 없는 결과가 만들어지게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;327&quot; data-start=&quot;278&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 아무! 와 같이 어색한 위치에서 문장이 종료되는 것 역시 같은 이유입니다.&lt;/p&gt;
&lt;p data-end=&quot;475&quot; data-start=&quot;329&quot; data-ke-size=&quot;size16&quot;&gt;uni-gram은 &amp;lt;EOS&amp;gt;가 얼마나 자주 등장하는지는 알 수 있습니다. 하지만 어떤 단어 뒤에서 종료되어야 자연스러운지는 전혀 알지 못합니다. 결국 종료 토큰 역시 단순 확률에 의해 선택되기 때문에, 문장이 엉뚱한 위치에서 끝나는 현상이 발생하게 됩니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;588&quot; data-start=&quot;477&quot; data-ke-size=&quot;size16&quot;&gt;정리해보자면, uni-gram은 개별 단어의 등장 빈도만 학습하는 구조입니다. 그리고 n의 차원이 증가할수록 단순한 단어 나열에서 벗어나, 이전 문맥을 조금씩 따라가는 방향으로 발전하게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-gram&lt;/h3&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;이제 2-gram으로 만들어보겠습니다.&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;562&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1VLby/dJMcafs5kga/yBeNHZckoECGhVJKDNlXp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1VLby/dJMcafs5kga/yBeNHZckoECGhVJKDNlXp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1VLby/dJMcafs5kga/yBeNHZckoECGhVJKDNlXp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1VLby%2FdJMcafs5kga%2FyBeNHZckoECGhVJKDNlXp1%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;435&quot; height=&quot;365&quot; data-origin-width=&quot;562&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'하고'로 시작하는 문장은 총 24,520회 등장하였으며, 전체 데이터 기준 약 0.3%로 1위를 차지하였습니다. 생각보다 굉장히 흥미로운 결과였습니다. 아마 문장 사이를 이어주는 접속 표현이나 설명형 문장에서 자주 사용되었기 때문으로 보입니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;184&quot; data-start=&quot;144&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 이제 이 데이터를 기반으로 실제 랜덤 문자열을 생성해보겠습니다.&amp;nbsp;&lt;br /&gt;모델이 커서 그런지 생각보다 오래걸리는 군요. 앞으로 3,4,5,6,7도 진행해야 하는데 걱정이 되는군요... ㅠㅠ&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1946&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MFXav/dJMb997tYyP/FmX2T8dvCDAouPgGF54sk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MFXav/dJMb997tYyP/FmX2T8dvCDAouPgGF54sk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MFXav/dJMb997tYyP/FmX2T8dvCDAouPgGF54sk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMFXav%2FdJMb997tYyP%2FFmX2T8dvCDAouPgGF54sk1%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;1946&quot; height=&quot;480&quot; data-origin-width=&quot;1946&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uni-gram&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;으로 생성했을 때와 비교하면 확실히 문장에 가까운 형태로 생성되는 것을 확인할 수 있었습니다. 실제로 단어들의 연결 역시 이전보다 훨씬 자연스러워졌습니다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하지만 생성된 결과를 자세히 읽어보면, 여전히 전체적으로 무슨 이야기를 하고 싶은 것인지는 이해하기 어려웠습니다. 즉, 문장 형태는 어느 정도 따라가기 시작했지만, 아직 의미와 흐름까지 연결되지는 못한 상태에 가까웠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div data-is-intersecting=&quot;true&quot; data-turn-id-container=&quot;request-6a01aeda-e668-83e8-ba06-822664e8cc17-29&quot;&gt;
&lt;div&gt;이제 조금씩 차원을 하나씩 더 올려보겠습니다.&lt;/div&gt;
&lt;div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3-gram&lt;/h3&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d11CnS/dJMcacwooqI/5CLgj5dVmeU8nUUGNJvUCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d11CnS/dJMcacwooqI/5CLgj5dVmeU8nUUGNJvUCk/img.png&quot; data-alt=&quot;분포도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d11CnS/dJMcacwooqI/5CLgj5dVmeU8nUUGNJvUCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd11CnS%2FdJMcacwooqI%2F5CLgj5dVmeU8nUUGNJvUCk%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;456&quot; height=&quot;355&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;470&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;1390&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2hCZK/dJMcaaZB8KM/s4ygOeu4W8Mz3aKyAo59K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2hCZK/dJMcaaZB8KM/s4ygOeu4W8Mz3aKyAo59K1/img.png&quot; data-alt=&quot;랜덤 문자열&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2hCZK/dJMcaaZB8KM/s4ygOeu4W8Mz3aKyAo59K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2hCZK%2FdJMcaaZB8KM%2Fs4ygOeu4W8Mz3aKyAo59K1%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;798&quot; height=&quot;272&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;랜덤 문자열&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-gram&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqTTMw/dJMcahkbLkK/u9OKjVHe5yNKgkjZSs2dsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqTTMw/dJMcahkbLkK/u9OKjVHe5yNKgkjZSs2dsk/img.png&quot; data-alt=&quot;분포도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqTTMw/dJMcahkbLkK/u9OKjVHe5yNKgkjZSs2dsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqTTMw%2FdJMcahkbLkK%2Fu9OKjVHe5yNKgkjZSs2dsk%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;453&quot; height=&quot;333&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;510&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;1796&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbcebh/dJMcagS5tWz/GBFNgE2dUAnoGYCLv7S121/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbcebh/dJMcagS5tWz/GBFNgE2dUAnoGYCLv7S121/img.png&quot; data-alt=&quot;랜덤 문자열&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbcebh/dJMcagS5tWz/GBFNgE2dUAnoGYCLv7S121/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcbcebh%2FdJMcagS5tWz%2FGBFNgE2dUAnoGYCLv7S121%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;783&quot; height=&quot;223&quot; data-origin-width=&quot;1796&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;랜덤 문자열&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4-gram까지 올리니 놀라운일이 발생하였습니다. 문장이 자연스럽게 이어졌습니다. 게다가 좀 짧은 문장들은 실제로 본문에서 찾아볼수 있었습니다. 예를 들어, '이번 사건은 나에게 가장 귀중한 교훈을 가르쳐 주었습니다'를 본문에서 조회를 해보면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QbZap/dJMb99M8WEh/IiPikLEkF2ALQfm9FoRKY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QbZap/dJMb99M8WEh/IiPikLEkF2ALQfm9FoRKY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QbZap/dJMb99M8WEh/IiPikLEkF2ALQfm9FoRKY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQbZap%2FdJMb99M8WEh%2FIiPikLEkF2ALQfm9FoRKY1%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;606&quot; height=&quot;287&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;450&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-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;227&quot; data-start=&quot;104&quot; data-ke-size=&quot;size16&quot;&gt;아마 문장이 길어질수록 단어 조합의 경우의 수가 급격하게 증가하기 때문으로 보입니다. 즉, 짧은 문장은 우연히 기존 데이터와 일치할 가능성이 있지만, 긴 문장은 새로운 조합으로 생성될 확률이 훨씬 높아지게 되는 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;227&quot; data-start=&quot;104&quot; data-ke-size=&quot;size23&quot;&gt;5-gram&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 부터는 원문을 더 많이 따라가는 구간으로 파일 크기와 희소성이 커지는 구간입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;희소성&lt;/b&gt;&lt;/p&gt;
&lt;div data-is-intersecting=&quot;true&quot; data-turn-id-container=&quot;request-6a01aeda-e668-83e8-ba06-822664e8cc17-31&quot;&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5&quot; data-message-id=&quot;396205a4-b8b0-4d6d-88fe-1d973b749fbe&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-end=&quot;46&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;가능한 조합의 수는 엄청나게 많지만, 실제로 관측된 조합은 극히 일부에 불과합니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 2-gram은 다음과 같은 형태입니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;나는 -&amp;gt; 갔다
나는 -&amp;gt; 보았다
나는 -&amp;gt; 말했다&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;239&quot; data-start=&quot;146&quot; data-ke-size=&quot;size16&quot;&gt;즉, 앞 단어 1개만 참고하기 때문에 동일한 문맥이 여러 번 반복됩니다. 따라서 하나의 문맥에서 등장할 수 있는 다음 단어 후보 역시 비교적 다양하게 존재하게 됩니다. 하지만 5-gram은 이야기가 달라집니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;그러나 은몽씨 나는 결코 -&amp;gt; 은몽씨가&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;394&quot; data-start=&quot;315&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 앞 단어 4개를 함께 참고하게 됩니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;문제는 이런 긴 문맥 조합은 대부분 코퍼스 안에서 단 한 번만 등장하는 경우가 많다는 점입니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;결국 모델은 다음과 같은 형태로 동작하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;그러나 은몽씨 나는 결코 -&amp;gt; 은몽씨가 (1회)
은몽씨 나는 결코 은몽씨가 -&amp;gt; 믿고 (1회)
나는 결코 은몽씨가 믿고 -&amp;gt; 있는것과 (1회)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;591&quot; data-start=&quot;530&quot; data-ke-size=&quot;size16&quot;&gt;이 상태가 되면 모델은 일반적인 패턴을 학습한다기보다, 원문 자체를 그대로 따라가는 방향에 가까워지게 됩니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;정리해보면 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;737&quot; data-start=&quot;610&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;673&quot; data-start=&quot;610&quot; data-section-id=&quot;1suck37&quot;&gt;N이 작다
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;673&quot; data-start=&quot;622&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;633&quot; data-start=&quot;622&quot; data-section-id=&quot;1mwmpv9&quot;&gt;문맥 정보는 약함&lt;/li&gt;
&lt;li data-end=&quot;650&quot; data-start=&quot;636&quot; data-section-id=&quot;niro9u&quot;&gt;하지만 후보 수는 많음&lt;/li&gt;
&lt;li data-end=&quot;673&quot; data-start=&quot;653&quot; data-section-id=&quot;wbn40c&quot;&gt;따라서 이상한 조합이 자주 등장함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;737&quot; data-start=&quot;675&quot; data-section-id=&quot;13gaymg&quot;&gt;N이 크다
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;737&quot; data-start=&quot;687&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;698&quot; data-start=&quot;687&quot; data-section-id=&quot;1mw7rod&quot;&gt;문맥 정보는 강함&lt;/li&gt;
&lt;li data-end=&quot;718&quot; data-start=&quot;701&quot; data-section-id=&quot;jyb27t&quot;&gt;하지만 후보 수는 매우 적음&lt;/li&gt;
&lt;li data-end=&quot;737&quot; data-start=&quot;721&quot; data-section-id=&quot;1g2auhl&quot;&gt;결국 원문 암기에 가까워짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;855&quot; data-start=&quot;739&quot; data-ke-size=&quot;size16&quot;&gt;즉, 5-gram에서 발생하는 희소성 문제(Sparsity)는 &quot;앞 단어 4개가 동일한 경우&quot; 자체가 거의 존재하지 않기 때문에, 다음 단어를 예측할 선택지가 급격하게 줄어드는 현상이라고 볼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W5ehH/dJMb990KJmd/bKeEFWbXHHEaGdeaKYh4Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W5ehH/dJMb990KJmd/bKeEFWbXHHEaGdeaKYh4Ck/img.png&quot; data-alt=&quot;분포도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W5ehH/dJMb990KJmd/bKeEFWbXHHEaGdeaKYh4Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW5ehH%2FdJMb990KJmd%2FbKeEFWbXHHEaGdeaKYh4Ck%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;543&quot; height=&quot;319&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;476&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;1948&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byhujI/dJMcad23UI4/oK1InhUilU2Uf7QgeOdde1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byhujI/dJMcad23UI4/oK1InhUilU2Uf7QgeOdde1/img.png&quot; data-alt=&quot;랜덤 문자열&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byhujI/dJMcad23UI4/oK1InhUilU2Uf7QgeOdde1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyhujI%2FdJMcad23UI4%2FoK1InhUilU2Uf7QgeOdde1%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;1948&quot; height=&quot;472&quot; data-origin-width=&quot;1948&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;랜덤 문자열&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 말씀드리자면, 차원을 계속 증가시켜 6-gram, 7-gram, 8-gram &amp;hellip; 11-gram까지 테스트하는 것은 큰 의미가 없다고 느껴졌습니다. 물론 문맥 정확도 자체는 계속 상승할 수 있습니다. 하지만 그만큼 희소성 문제 역시 더욱 심해지게 됩니다. 결국, 모델은 새로운 패턴을 예측한다기보다, 기존 원문을 그대로 외우고 재현하는 방향에 가까워지게 됩니다. 즉, 문맥을 일반화해서 이해하는 것이 아니라 긴 문자열 패턴 자체를 기억하는 형태가 되어버리는 것입니다. 이렇게 된다면 언어 모델이 가져야 하는 &quot;새로운 문장을 생성하고 예측하는 능력&quot;은 오히려 약해질 수 있다고 느껴졌습니다. 결국 단순히 N을 높이는 것만으로는 좋은 언어 모델이 만들어지는 것은 아니라는 점을 확인할 수 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 시간에는 사전 데이터를 기반으로 N-gram을 실습해보았습니다. 하지만 실제 LLM은 단순 사전 데이터가 아니라, 실제 문서와 문장 데이터를 기반으로 학습이 이루어진다는 점에서 차이가 존재하였습니다. 그래서 이번에는 문학 데이터를 직접 수집하여 N-gram을 다시 실습해보았습니다. uni-gram부터 시작하여 5-gram까지 차례대로 실험을 진행해보니, 초반에는 새로운 문자열을 생성해내는 모습을 확인할 수 있었습니다. 하지만 단어의 위치와 문맥이 뒤죽박죽이었기 때문에, 실제로 읽어보면 무슨 의미인지 이해하기 어려운 경우가 대부분이었습니다. 반대로 차원을 계속 증가시키자 훨씬 자연스러운 문장이 생성되기 시작하였습니다. 하지만 이번에는 또 다른 문제가 발생하였습니다. 문장이 자연스러워질수록 새로운 문장을 생성한다기보다, 원문 자체를 그대로 따라가는 형태에 가까워졌기 때문입니다. 결국 희소성 문제(Sparsity) 역시 함께 증가하게 되었고, 모델은 점점 더 긴 패턴을 기억하는 방향으로 동작하게 되었습니다. 개인적으로는 이 부분이 굉장히 아이러니하게 느껴졌습니다. 일반적으로 LLM이라면 새로운 문장을 창조할 것이라고 기대하게 됩니다. 하지만 N을 계속 증가시키게 되면 오히려 창의성과는 점점 거리가 멀어지고, 학습 데이터를 재현하는 방향에 가까워졌기 때문입니다. 그렇다고 해서 차원을 낮추게 되면 이번에는 문맥 자체를 거의 이해하지 못한 채, 의미를 알 수 없는 문장들만 생성하게 됩니다. 제가 느끼기에는 이것이 N-gram의 가장 큰 한계처럼 보였습니다. 그럼에도 불구하고 데이터를 기반으로 다음 데이터를 예측한다는 핵심 아이디어 자체는 굉장히 흥미로웠습니다. 실제로 이후 등장한 다양한 언어 모델들 역시, 결국은 이전 문맥을 기반으로 다음 데이터를 예측한다는 방향성 자체는 이어지고 있다고 느껴졌습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;출처&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://www.honglab.ai/courses/llmpt1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.honglab.ai/courses/llmpt1&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발/LLM</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/579</guid>
      <comments>https://b-programmer.tistory.com/579#entry579comment</comments>
      <pubDate>Sun, 17 May 2026 01:20:25 +0900</pubDate>
    </item>
    <item>
      <title>마이바티스 매퍼 방식은 어떤것을 사용해야 할까?</title>
      <link>https://b-programmer.tistory.com/578</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;마이바티스에서는 SQL을 매핑하는 방식으로 크게 어노테이션 방식과 XML 방식을 제공하고 있습니다. 저는 이 중 어노테이션 방식을 선택하게 되었습니다. 가장 큰 이유는 멀티 모듈 구조에서 조금 더 관리하기 좋다고 느꼈기 때문입니다. 특히 어떤 기능이 어디에 존재하는지 추적하는 과정에서 어노테이션 방식이 더 직관적이라고 생각했습니다. 물론, 단점도 존재합니다. 복잡한 동적 쿼리를 작성하게 되면 XML 방식에 비해 불편함이 생길 수 있습니다. 그럼에도 불구하고 어노테이션 방식을 선택한 이유와, 왜 멀티 모듈 환경에서 잘 어울린다고 생각했는지 이번 글에서 정리해보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어노테이션 방식은 어떻게 사용할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어노테이션 방식은 Mapper 인터페이스 위에 SQL을 직접 작성하여 사용하는 방식입니다.&lt;br /&gt;기존 XML처럼 별도의 mapper.xml 파일을 생성하는 것이 아니라, 자바 코드 내부에서 SQL을 함께 관리하게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;156&quot; data-start=&quot;124&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 간단한 조회는 다음과 같이 작성할 수 있습니다.&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6VuWc/dJMcagS324M/NOGKkhQjTJ6gYdYKo95oO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6VuWc/dJMcagS324M/NOGKkhQjTJ6gYdYKo95oO1/img.png&quot; data-alt=&quot;빨간건 스키마가 없기때문이다. 예제라 스키마는 추가하지 않았다. 실제로 동작하는건 중요하지 않는다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6VuWc/dJMcagS324M/NOGKkhQjTJ6gYdYKo95oO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6VuWc%2FdJMcagS324M%2FNOGKkhQjTJ6gYdYKo95oO1%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;636&quot; height=&quot;233&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;빨간건 스키마가 없기때문이다. 예제라 스키마는 추가하지 않았다. 실제로 동작하는건 중요하지 않는다.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-end=&quot;450&quot; data-start=&quot;331&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 작성하게 되면 Mapper 인터페이스만 확인해도 어떤 SQL이 실행되는지 바로 확인할 수 있습니다.&lt;br /&gt;즉, XML 파일로 이동하지 않아도 되기 때문에 코드 흐름을 따라가기 조금 더 수월하다고 느껴졌습니다.&lt;/p&gt;
&lt;p data-end=&quot;605&quot; data-start=&quot;452&quot; data-ke-size=&quot;size16&quot;&gt;또한 멀티 모듈 환경에서는 mapper.xml 파일이 resources 하위로 분리되면서 위치를 추적해야 하는 경우가 생길 수 있습니다. 반면 어노테이션 방식은 인터페이스와 SQL이 함께 존재하기 때문에, 도메인이 증가하더라도 상대적으로 구조를 파악하기 쉽다고 느껴졌습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;737&quot; data-start=&quot;607&quot; data-ke-size=&quot;size16&quot;&gt;다만 SQL이 길어질수록 가독성이 급격하게 떨어질 수 있다는 단점 역시 존재합니다.&lt;br /&gt;특히 동적 쿼리가 많아질수록 XML 방식이 더 적합할 수 있기 때문에, 프로젝트 상황에 따라 혼합하여 사용하는 것도 고려할 수 있다고 생각합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 Annotation 방식을 선택한 이유가 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 생각하는 Annotation 방식의 가장 큰 장점은 바로 &quot;추적&quot;입니다. 만약, XML 방식으로 작성하게 된다면 아래와 같이 resources 하위에 mapper xml 파일이 생성됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;606&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6UWsT/dJMcafGC6oL/D0AgVgUQdyEsntkQ3D9qB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6UWsT/dJMcafGC6oL/D0AgVgUQdyEsntkQ3D9qB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6UWsT/dJMcafGC6oL/D0AgVgUQdyEsntkQ3D9qB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6UWsT%2FdJMcafGC6oL%2FD0AgVgUQdyEsntkQ3D9qB0%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;606&quot; height=&quot;198&quot; data-origin-width=&quot;606&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 user 도메인에 대한 매퍼를 찾는다고 가정해보겠습니다. 이 경우 app 영역에서는 자바 코드를 따라가다가, 실제 SQL을 확인하려면 다시 infrastructure 모듈의 resources 영역으로 이동해야 합니다. 특히 멀티 모듈 구조에서는 infrastructure 모듈에 여러 설정 파일과 라이브러리 관련 리소스가 함께 존재하는 경우가 많습니다. 즉, 개발자는 자바 코드와 resources 영역을 계속 오가며 흐름을 추적해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게다가 resources 영역에는 mapper xml 뿐만 아니라 application.yml 같은 설정 파일도 함께 존재합니다. 결국 도메인이 증가할수록 &quot;어떤 기능이 어디에 존재하는지&quot; 파악하는 비용 역시 함께 증가한다고 느꼈습니다. 반면 Annotation 방식은 인터페이스와 SQL이 함께 존재하기 때문에, 코드 흐름을 따라가기가 조금 더 직관적이라고 생각했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;732&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NVA1y/dJMcabqHaWk/khcLYebrsUE0vA5mstlfw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NVA1y/dJMcabqHaWk/khcLYebrsUE0vA5mstlfw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NVA1y/dJMcabqHaWk/khcLYebrsUE0vA5mstlfw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNVA1y%2FdJMcabqHaWk%2FkhcLYebrsUE0vA5mstlfw1%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;458&quot; height=&quot;453&quot; data-origin-width=&quot;732&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유로 멀티 모듈 환경에서 마이바티스를 적용할 때 Annotation 방식을 선호하게 되었습니다.&lt;/p&gt;
&lt;p data-end=&quot;381&quot; data-start=&quot;266&quot; data-ke-size=&quot;size16&quot;&gt;하지만 여기서 한 가지 의문이 생깁니다.&lt;br /&gt;최근에는 AI를 활용해 코드 흐름을 추적하는 경우가 많아졌습니다.&lt;br /&gt;그렇다면 &quot;추적이 쉽다&quot;는 Annotation 방식의 장점은 이전보다 약해진 것이 아닐까요?&lt;/p&gt;
&lt;p data-end=&quot;517&quot; data-start=&quot;383&quot; data-ke-size=&quot;size16&quot;&gt;AI 입장에서는 SQL이 Annotation에 있든 XML에 있든, 관련 파일을 함께 읽어낼 수 있다면 큰 차이가 없을 수 있습니다.&lt;br /&gt;그럼에도 불구하고 멀티 모듈 환경에서 Annotation 방식을 선택하는 것이 여전히 의미가 있을까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI 시대에 이 선택이 여전히 의미가 있는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 불구하고 의미는 있다고 생각합니다. 이유는 AI가 코드를 작성하고 리뷰를 도와주더라도, 최종적으로 그 코드를 판단하는 주체는 사람이기 때문입니다. AI는 XML과 Annotation을 모두 읽을 수 있지만, 사람은 최종 리뷰어로서 변경된 코드가 적절한지 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Annotation 방식은 Mapper 인터페이스와 SQL이 함께 존재하기 때문에, 리뷰하는 입장에서 변경 범위를 더 빠르게 파악할 수 있습니다. 즉, 단순히 &quot;AI가 찾을 수 있느냐&quot;의 문제가 아니라, 사람이 최종적으로 검토할 때 얼마나 적은 맥락 이동으로 판단할 수 있느냐의 문제라고 생각했습니다. 그래서 AI가 개발을 도와주는 환경에서도 Annotation 방식은 여전히 의미가 있다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히, 멀티 모듈처럼 구조가 나뉘어 있는 환경에서는 코드와 SQL이 가까이 있는 것만으로도 최종 리뷰 비용을 줄일 수 있다고 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지&amp;nbsp;멀티&amp;nbsp;모듈&amp;nbsp;환경에서&amp;nbsp;Annotation&amp;nbsp;방식을&amp;nbsp;선택한&amp;nbsp;이유에&amp;nbsp;대해&amp;nbsp;알아봤습니다.&lt;br /&gt;그렇다면 이 선택을 하면서 어떤 것을 포기해야 할까요? Annotation 방식은 코드와 SQL을 가까이 둘 수 있다는 장점이 있지만, 그만큼 XML 방식이 가지고 있던 장점 일부를 내려놓아야 합니다. 특히 복잡한 동적 쿼리를 작성하거나 SQL을 별도로 관리해야 하는 상황에서는 Annotation 방식이 오히려 불편하게 느껴질 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Annotation 방식을 선택하면 포기해야 하는 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 XML 방식이 가장 강점을 가지는 부분은 무엇일까요?&lt;br /&gt;바로 복잡한 SQL과 동적 쿼리를 관리하는 부분입니다. &lt;br /&gt;특히 문자열 내부에 조건문이나 반복문이 계속 추가되는 구조에서는 Annotation 방식보다 XML 방식이 훨씬 깔끔하게 느껴질 수 있습니다. Annotation 방식 역시 동적 쿼리를 작성할 수는 있습니다. 하지만 문자열 내부에 다시 문자열을 작성해야 하는 구조가 반복되다 보니, 쿼리가 복잡해질수록 가독성이 빠르게 떨어질 수 있습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;510&quot; data-start=&quot;441&quot; data-ke-size=&quot;size16&quot;&gt;반면 XML 방식은 SQL 자체를 분리해서 관리하기 때문에, 복잡한 쿼리일수록 오히려 구조를 파악하기 쉬워진다고 느꼈습니다.&lt;/p&gt;
&lt;p data-end=&quot;187&quot; data-start=&quot;164&quot; data-ke-size=&quot;size16&quot;&gt;여기서 선택지는 크게 두 가지가 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;289&quot; data-start=&quot;189&quot; data-ke-size=&quot;size16&quot;&gt;첫 번째는 XML과 Annotation을 함께 사용하는 하이브리드 방식입니다.&lt;br /&gt;단순한 쿼리는 Annotation으로 작성하고, 복잡한 동적 쿼리는 XML로 분리하는 방식입니다.&lt;/p&gt;
&lt;p data-end=&quot;435&quot; data-start=&quot;291&quot; data-ke-size=&quot;size16&quot;&gt;두 번째는 Annotation 방식을 유지하되, SQL Provider와 같은 별도의 클래스를 사용하는 방식입니다.&lt;br /&gt;이 경우 기존 Mapper 인터페이스 안에서 모든 SQL을 처리하는 것이 아니라, SQL을 생성하는 책임을 다른 클래스로 분리하게 됩니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;551&quot; data-start=&quot;437&quot; data-ke-size=&quot;size16&quot;&gt;결국 Annotation 방식을 선택한다는 것은 단순히 XML을 사용하지 않는다는 의미가 아닙니다.&lt;br /&gt;복잡한 쿼리가 등장했을 때, 그 복잡도를 어디로 옮길 것인지 함께 고민해야 한다는 의미에 가깝습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;510&quot; data-start=&quot;441&quot; data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;글에서는&amp;nbsp;후자의&amp;nbsp;방법을&amp;nbsp;중심으로&amp;nbsp;고민해보려고&amp;nbsp;합니다.&lt;br /&gt;&lt;br /&gt;Annotation&amp;nbsp;방식을&amp;nbsp;유지하면서도&amp;nbsp;복잡한&amp;nbsp;쿼리를&amp;nbsp;어떻게&amp;nbsp;다룰&amp;nbsp;수&amp;nbsp;있을지&amp;nbsp;살펴보고,&amp;nbsp;어느&amp;nbsp;정도&amp;nbsp;복잡도가&amp;nbsp;높아졌을&amp;nbsp;때&amp;nbsp;XML&amp;nbsp;방식을&amp;nbsp;고려해야&amp;nbsp;하는지도&amp;nbsp;함께&amp;nbsp;정리해보겠습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;510&quot; data-start=&quot;441&quot; data-ke-size=&quot;size23&quot;&gt;Annotation 방식을 유지하면서 복잡한 쿼리를 다루는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Annotation 방식을 살펴보기 이전에&amp;nbsp; XML은 어떻게 복잡한 쿼리를 다루는지 간단하게 알아봅시다.&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;1134&quot; data-origin-height=&quot;868&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUm4cx/dJMcaaegyPX/r0o37haqH0bVTNKaJHkfPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUm4cx/dJMcaaegyPX/r0o37haqH0bVTNKaJHkfPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUm4cx/dJMcaaegyPX/r0o37haqH0bVTNKaJHkfPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUm4cx%2FdJMcaaegyPX%2Fr0o37haqH0bVTNKaJHkfPk%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;602&quot; height=&quot;461&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;868&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 코드는 CTE(WITH 절)와 서브쿼리를 이용해 사용자별 주문 합계를 구한 뒤, 주문 금액을 기준으로 오름차순 정렬하는 쿼리입니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그렇다면 이 쿼리를 Annotation 방식으로 작성하면 어떻게 될까요?&lt;/span&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;1282&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K21Lt/dJMcac4a6CT/8EcnpgbEXrb7yZ7RhQJTaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K21Lt/dJMcac4a6CT/8EcnpgbEXrb7yZ7RhQJTaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K21Lt/dJMcac4a6CT/8EcnpgbEXrb7yZ7RhQJTaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK21Lt%2FdJMcac4a6CT%2F8EcnpgbEXrb7yZ7RhQJTaK%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;632&quot; height=&quot;446&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼&amp;nbsp;순수&amp;nbsp;SQL만&amp;nbsp;존재하는&amp;nbsp;경우에는&amp;nbsp;Annotation&amp;nbsp;방식에서도&amp;nbsp;거의&amp;nbsp;그대로&amp;nbsp;옮겨&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;다만 XML에서 사용하던 SQL을 그대로 복사하는 경우에는 XML 엔티티 escape를 확인해야 합니다. 예를 들어, &amp;amp;gt;는 &amp;gt;로, &amp;amp;lt;는 &amp;lt;로 변경해줘야 합니다. 즉, XML 내부에서 사용하던 쿼리를 Annotation 방식으로 옮길 때는 단순히 복사해서 붙여넣는 것이 아니라, XML 엔티티 escape가 남아 있지는 않은지 확인하는 과정이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, &amp;lt;if&amp;gt; &amp;lt;foreach&amp;gt; &amp;lt;where&amp;gt;을 사용하는 경우에는 어떨까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;1122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkVA2V/dJMcacQEs65/sFn1j6cKvfDzC86M86rJW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkVA2V/dJMcacQEs65/sFn1j6cKvfDzC86M86rJW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkVA2V/dJMcacQEs65/sFn1j6cKvfDzC86M86rJW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkVA2V%2FdJMcacQEs65%2FsFn1j6cKvfDzC86M86rJW1%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;650&quot; height=&quot;593&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;1122&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이처럼 조건에 따라 SQL이 동적으로 변경되는 쿼리를 동적 쿼리라고 합니다. &lt;/span&gt;&lt;br /&gt;&lt;span&gt;마이바티스 XML 방식에서는 if, choose, foreach 같은 태그를 이용해 이러한 동적 쿼리를 처리할 수 있습니다.&amp;nbsp;&lt;br /&gt;그렇다면, Annotation 방식에도 똑같이 적용해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1342&quot; data-origin-height=&quot;1234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/la2CV/dJMcabqIbhM/XaE5PtBGvjy4kK8kJYT5T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/la2CV/dJMcabqIbhM/XaE5PtBGvjy4kK8kJYT5T1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/la2CV/dJMcabqIbhM/XaE5PtBGvjy4kK8kJYT5T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fla2CV%2FdJMcabqIbhM%2FXaE5PtBGvjy4kK8kJYT5T1%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;663&quot; height=&quot;610&quot; data-origin-width=&quot;1342&quot; data-origin-height=&quot;1234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 XML에서 사용하던 태그를 Annotation 방식에 그대로 옮기자 컴파일 예외가 발생했습니다. &lt;/span&gt;&lt;span&gt;그렇다면 왜 Annotation 방식에서는 XML 태그를 그대로 사용할 수 없을까요? &lt;/span&gt;근본적인 이유는 Annotation 방식은 문자열이고, XML 방식은 문서 구조이기 때문입니다.&lt;br /&gt;&lt;br /&gt;XML 방식에서는 &amp;lt;if&amp;gt;, &amp;lt;foreach&amp;gt; 같은 태그 자체가 XML 구조의 일부로 인식됩니다. 그래서 마이바티스가 해당 태그를 읽고 동적 SQL로 변환할 수 있습니다. 반면, Annotation 방식에서는 SQL이 단순 문자열로 전달됩니다. 즉, &amp;lt;if&amp;gt; 같은 내용이 들어가더라도 구조가 아니라&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;문자열로 취급되기 때문에 그대로 해석할 수 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;문자열은 취급할&amp;nbsp; 수 없는 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 문자열로 취급을 하면 어째서 해석하지 못하는 걸까요? &lt;span&gt;문자열은 단순 데이터이기 때문입니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;예를 들어 Annotation 방식에서는 SQL이 아래처럼 자바 문자열로 전달됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778855967320&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java
@Select(&quot;select * from user&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 자바 입장에서는 이것을 단순한 문자 집합으로만 인식합니다. &amp;lt;if&amp;gt; 역시 마찬가지입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1778855996762&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Select(&quot;&amp;lt;if test='id != null'&amp;gt; ... &amp;lt;/if&amp;gt;&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성하더라도 자바는 이것이 조건문 구조인지 알지 못합니다. 그냥 &amp;lt;, i, f 로 이루어진 문자열일 뿐입니다.반면, XML 방식에서는 &amp;lt;if&amp;gt;가 문자열이 아니라 XML 문서의 태그 구조로 존재합니다. 그래서 마이바티스가 XML을 파싱하면서 이건 조건문이구나라고 해석할 수 있습니다. 즉, 핵심은 단순합니다. 문자열은 의미를 가지지 않지만, XML 태그는 구조를 가지기 때문에 마이바티스가 동적 SQL로 해석할 수 있는 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면, Annotation방법을 사용할때는 동적 쿼리를 사용하지 못할까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다행스럽게도 Annotation 방식에서도 동적 쿼리를 사용할 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;다만 XML 방식처럼 태그를 바로 사용하는 것이 아니라, 마이바티스가 동적 SQL로 해석할 수 있는 형태로 만들어줘야 합니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;그 과정에 대해 조금 더 자세히 확인해보겠습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;마이바티스에서는 이러한 문제를 Provider를 통해 해결할 수 있습니다. &lt;/span&gt;&lt;br /&gt;&lt;span&gt;기존 Mapper 인터페이스 내부에 모든 SQL을 작성하는 것이 아니라, 별도의 클래스에서 동적 SQL을 생성하도록 분리하는 방식입니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;즉, Annotation 방식은 유지하면서도 복잡한 동적 쿼리를 처리할 수 있도록 만들어주는 구조라고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nj2O4/dJMcahYJosu/RarhoKLDDF1syAu4cZ4p70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nj2O4/dJMcahYJosu/RarhoKLDDF1syAu4cZ4p70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nj2O4/dJMcahYJosu/RarhoKLDDF1syAu4cZ4p70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnj2O4%2FdJMcahYJosu%2FRarhoKLDDF1syAu4cZ4p70%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;1630&quot; height=&quot;682&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, Annotation 방식을 유지하면서도 복잡한 동적 쿼리를 처리할 수 있도록 만들어주는 구조라고 볼 수 있습니다. &lt;/span&gt;&lt;span&gt;코드를 확인해보면 SQL 객체를 생성한 뒤, SELECT, FROM, WHERE 절에 해당하는 메소드를 사용하는 것을 볼 수 있습니다.&amp;nbsp; &lt;/span&gt;&lt;span&gt;또한, 자바 코드 기반으로 동작하기 때문에, 전달받은 파라미터 값을 기준으로 조건을 분기하여 동적 쿼리를 구성할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 이것은 실제 SQL 자체가 아닙니다. &lt;/span&gt;&lt;span&gt;정확히는 SQL을 생성하기 위한 Provider 클래스에 가깝습니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그렇다면 이렇게 생성한 SQL은 어디에서 사용되는 것일까요? &lt;/span&gt;&lt;span&gt;마이바티스는 Mapper 인터페이스에서 Provider 클래스를 참조하여, 실행 시점에 동적으로 SQL을 생성한 뒤 실제 쿼리로 변환하여 사용하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BY47X/dJMcafmlvSK/u8I4pv4bHd0KE28I9DaKak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BY47X/dJMcafmlvSK/u8I4pv4bHd0KE28I9DaKak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BY47X/dJMcafmlvSK/u8I4pv4bHd0KE28I9DaKak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBY47X%2FdJMcafmlvSK%2Fu8I4pv4bHd0KE28I9DaKak%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;1270&quot; height=&quot;138&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이를 해석해보면 @SelectProvider를 통해 Provider 클래스가 생성한 SQL을 실행하라는 의미입니다. &lt;/span&gt;&lt;span&gt;즉, Mapper 인터페이스 내부에서 직접 SQL을 작성하는 것이 아니라, 별도의 Provider 클래스에서 생성한 SQL을 가져와 실행하는 구조라고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nj2O4/dJMcahYJosu/RarhoKLDDF1syAu4cZ4p70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nj2O4/dJMcahYJosu/RarhoKLDDF1syAu4cZ4p70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nj2O4/dJMcahYJosu/RarhoKLDDF1syAu4cZ4p70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnj2O4%2FdJMcahYJosu%2FRarhoKLDDF1syAu4cZ4p70%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;1630&quot; height=&quot;682&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다시 코드를 자세히 확인해보면 문장 끝마다 `;`를 붙이고 있는 것을 볼 수 있습니다. &lt;/span&gt;&lt;span&gt;즉, 결국 SQL을 문자열이 아닌 자바 코드 형태로 조합하고 있다는 뜻입니다. &lt;/span&gt;&lt;span&gt;이 때문에 조건이 많아지고 쿼리 복잡도가 올라갈수록 &lt;u&gt;가독성&lt;/u&gt;이 빠르게 떨어질 수 있습니다. &lt;/span&gt;&lt;span&gt;특히 단순 SQL이 아니라 여러 조건과 분기가 추가되기 시작하면, 실제 SQL을 작성하는 것보다 자바 문법을 맞추는 데 더 집중하게 되는 경우도 발생할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 수준의 복잡도에서는 충분히 사용할 수 있는 방식이라고 느껴졌습니다. &lt;/span&gt;&lt;span&gt;하지만 쿼리 복잡도가 계속 증가하는 경우에는 Annotation 방식만 고집하기보다는, XML 방식을 함께 사용하는 하이브리드 구조 역시 고려할 필요가 있다고 생각합니다. &lt;/span&gt;&lt;span&gt;특히 동적 조건이 많아지고 SQL 길이가 길어질수록 XML 방식이 오히려 더 읽기 쉽고 유지보수에 유리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span&gt;특정 구조나 기술을 선택할 때는 단순히 유명한 기술인가를 보는 것이 아니라, 왜 이 구조를 선택했는지와 어떤 트레이드오프가 존재하는지를 함께 고민해야 한다고 생각합니다. &lt;/span&gt;&lt;span&gt;저 역시 SQL 매퍼 기술을 선택하는 과정에서 ORM인 JPA 대신 마이바티스를 선택하였습니다. &lt;/span&gt;&lt;span&gt;그 이유는 현재 프로젝트에서는 마이바티스가 JPA보다 SQL 흐름을 직접 제어하고 접근하기 더 쉽다고 느꼈기 때문입니다. &lt;/span&gt;&lt;span&gt;또한 프로젝트 구조를 멀티 모듈로 구성하였기 때문에, Annotation 방식이 XML 방식보다 더 잘 맞는다고 생각하였습니다. 같은 Java 코드 흐름 안에서 SQL을 함께 관리할 수 있기 때문에, 구조를 추적하고 흐름을 이해하기 조금 더 직관적이라고 느꼈습니다. &lt;/span&gt;&lt;span&gt;하지만 동적 쿼리를 작성하기 시작하면서 문제 역시 존재하였습니다. &lt;/span&gt;&lt;span&gt;복잡한 조건이 증가할수록 XML 방식보다 Annotation 방식의 복잡도가 더 빠르게 증가하였기 때문입니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그럼에도 불구하고 Annotation 방식을 선택한 이유는, 동적 쿼리를 작성하는 복잡성보다 프로젝트 전체 흐름을 추적하고 제어하는 것이 더 중요하다고 판단하였기 때문입니다. &lt;/span&gt;&lt;span&gt;물론, 프로젝트 규모와 복잡도에 따라 XML 방식이 더 적절한 상황 역시 충분히 존재할 수 있습니다. 결국 중요한 것은 어떤 방식이 무조건 더 좋다가 아니라, 현재 프로젝트 구조에서 어떤 방식이 더 적절한지를 계속 고민하는 것이라고 생각합니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발/이력관리</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/578</guid>
      <comments>https://b-programmer.tistory.com/578#entry578comment</comments>
      <pubDate>Sat, 16 May 2026 01:00:49 +0900</pubDate>
    </item>
    <item>
      <title>N-gram 이해해보기</title>
      <link>https://b-programmer.tistory.com/577</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;TL;DR; N그램을 설명한 글입니다.&lt;/p&gt;
&lt;figure id=&quot;og_1778683329122&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;LLM from Scratch - Part 1. Statistical Language Models&quot; data-og-description=&quot;[LLM 바닥부터 만들기 - 파트1. 통계적 언어 모델의 원리] 부담 없이 즐기며 배우는 LLM 입문 강의  오픈이벤트 50%할인! (~5/17이후 가격이 인상됩니다.)&quot; data-og-host=&quot;www.honglab.ai&quot; data-og-source-url=&quot;https://www.honglab.ai/courses/llmpt1&quot; data-og-url=&quot;https://www.honglab.ai/courses/llmpt1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cvqo2z/dJMb9jOrLWQ/EoSfYtAmJDri4kbHtS0FnK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/gnok2/dJMb9b3WT7S/kJWcKuKrBt7ZJOJmPFSQTk/img.png?width=1161&amp;amp;height=1548&amp;amp;face=445_592_541_698,https://scrap.kakaocdn.net/dn/bALzkj/dJMb9kT7PFJ/9TL1xhTT8w6zICUyfLfXG1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450&quot;&gt;&lt;a href=&quot;https://www.honglab.ai/courses/llmpt1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.honglab.ai/courses/llmpt1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cvqo2z/dJMb9jOrLWQ/EoSfYtAmJDri4kbHtS0FnK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/gnok2/dJMb9b3WT7S/kJWcKuKrBt7ZJOJmPFSQTk/img.png?width=1161&amp;amp;height=1548&amp;amp;face=445_592_541_698,https://scrap.kakaocdn.net/dn/bALzkj/dJMb9kT7PFJ/9TL1xhTT8w6zICUyfLfXG1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450');&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;LLM from Scratch - Part 1. Statistical Language Models&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[LLM 바닥부터 만들기 - 파트1. 통계적 언어 모델의 원리] 부담 없이 즐기며 배우는 LLM 입문 강의  오픈이벤트 50%할인! (~5/17이후 가격이 인상됩니다.)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.honglab.ai&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N-gram의 아이디어는 1948년 &lt;span&gt;&lt;span&gt;Claude Shannon&lt;/span&gt;&lt;/span&gt; 의 정보이론 논문에서 시작되었습니다. 핵심은 단순합니다. &quot;다음 글자를 이전 글자를 기반으로 예측할 수 있지 않을까?&quot; 라는 아이디어였습니다. 이후 1980년대에 들어서며 컴퓨터 성능이 발전하였고, 이러한 통계 기반 언어 모델을 실제로 활용할 수 있는 환경이 만들어지기 시작하였습니다. 그렇게 N-gram은 음성 인식, 자동 완성, 기계 번역과 같은 다양한 자연어 처리 분야에서 활용되기 시작하였습니다. 현대의 LLM은 N-gram보다 훨씬 복잡한 구조를 사용하지만, &quot;이전 문맥을 기반으로 다음 단어를 예측한다&quot;는 핵심적인 방향성 자체는 크게 다르지 않습니다. 그렇다면 이런 의문이 들 수 있습니다. &quot;이렇게 단순한 아이디어인데, 왜 실제로 이해하고 학습하는 것은 어려울까?&quot; 겉으로 보면 단순히 다음 단어를 연결하는 방식처럼 보입니다. 하지만 N이 증가할수록 경우의 수가 급격하게 증가하게 되고, 데이터 부족 문제 역시 함께 발생하게 됩니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;802&quot; data-start=&quot;723&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 N-gram이 어떤 방식으로 동작하는지, 그리고 왜 단순해 보이는 구조가 생각보다 복잡한 문제를 가지게 되는지 확인해보겠습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;802&quot; data-start=&quot;723&quot; data-ke-size=&quot;size26&quot;&gt;그래서 N그램이 뭔데?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N-gram은 이전 글자를 기반으로 다음 글자를 예측하는 방법입니다. 예를 들어 토큰이 가 &amp;rarr; 나 &amp;rarr; 다 이렇게 존재한다고 가정해봅시다. 그렇다면 가 다음에는 무엇이 올까요? 바로 나입니다. 그렇다면 나 다음에는 무엇이 올까요? 바로 다입니다.&lt;/p&gt;
&lt;p data-end=&quot;330&quot; data-start=&quot;283&quot; data-ke-size=&quot;size16&quot;&gt;즉, 핵심은 단순합니다. 이전에 등장한 패턴을 기반으로 다음 글자를 예측하는 것입니다. 하지만 여기서부터 헷갈리기 시작합니다.&lt;/p&gt;
&lt;p data-end=&quot;388&quot; data-start=&quot;355&quot; data-ke-size=&quot;size16&quot;&gt;&quot;AI는 이런 단순한 연결만 보고 어떻게 학습한다는 거지?&quot;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;487&quot; data-start=&quot;390&quot; data-ke-size=&quot;size16&quot;&gt;실제로 N-gram은 우리가 생각하는 것보다 훨씬 단순한 구조입니다. 문장을 이해한다기보다, 이전에 등장했던 패턴과 확률을 기반으로 다음 글자를 이어붙이는 방식에 가깝습니다.&lt;/p&gt;
&lt;div data-is-intersecting=&quot;true&quot; data-turn-id-container=&quot;request-WEB:6faa0df8-a8d3-4c08-a7c7-ed877f95dec4-24&quot;&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5&quot; data-message-id=&quot;91341a08-7a55-417f-b3f3-3f8829f7143e&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-end=&quot;310&quot; data-start=&quot;238&quot; data-ke-size=&quot;size16&quot;&gt;지금 예시는 하나의 경우만 존재하기 때문에 단순해 보입니다. 하지만 경우의 수가 수십, 수백 개로 증가한다면 이야기는 달라집니다.&lt;/p&gt;
&lt;p data-end=&quot;333&quot; data-start=&quot;312&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 이런 의문이 들 수 있습니다. &quot;모든 경우를 전부 조합해서 계산하는 것일까?&quot;&lt;/p&gt;
&lt;p data-end=&quot;468&quot; data-start=&quot;363&quot; data-ke-size=&quot;size16&quot;&gt;결론부터 말하면 완전히 틀린 생각은 아닙니다. 실제로&lt;u&gt; N-gram은 이전에 등장했던 패턴들을 저장&lt;/u&gt;하고, 현재 상태에서 다음에 등장할 가능성이 높은 토큰을 찾아 이어붙이는 방식에 가깝습니다. 즉, 문장을 이해한다기보다 이전에 등장했던 연결 관계를 기반으로 다음 토큰을 예측하는 구조라고 볼 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;468&quot; data-start=&quot;363&quot; data-ke-size=&quot;size23&quot;&gt;아니 잠깐만 이거 끝말잇기랑 유사한 느낌인데...&lt;/h3&gt;
&lt;p data-end=&quot;139&quot; data-start=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;N-gram은 현재 상태에서 다음에 등장할 가능성이 높은 토큰을 찾아 이어붙이는 방식이라고 하였습니다. 그렇다면 우리가 익숙하게 알고 있는 '끝말잇기'와 비슷한 느낌이라고 볼 수도 있지 않을까요?&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;266&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1iP09/dJMcacQBRdl/Cwx7uRkC2WfZJHwJ6tA3zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1iP09/dJMcacQBRdl/Cwx7uRkC2WfZJHwJ6tA3zK/img.png&quot; data-alt=&quot;출처: https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC:%EB%81%9D%EB%A7%90%EC%9E%87%EA%B8%B0.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1iP09/dJMcacQBRdl/Cwx7uRkC2WfZJHwJ6tA3zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1iP09%2FdJMcacQBRdl%2FCwx7uRkC2WfZJHwJ6tA3zK%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;266&quot; height=&quot;190&quot; data-origin-width=&quot;266&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC:%EB%81%9D%EB%A7%90%EC%9E%87%EA%B8%B0.png&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;335&quot; data-start=&quot;271&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 끝말잇기 역시 현재 단어를 기준으로 다음에 어떤 단어가 이어질 수 있는지를 생각하게 됩니다. 만약 다음에 등장할 가능성이 높은 단어들을 어느 정도 예측할 수 있다면, 게임을 훨씬 유리하게 풀어나갈 수 있을 것입니다. &lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;N-gram 역시 이와 유사하게 이전에 등장했던 패턴들을 기반으로 다음 토큰을 예측하는 방식이라고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;458&quot; data-start=&quot;308&quot; data-ke-size=&quot;size16&quot;&gt;이 개념을 조금 더 직관적으로 이해하기 위해, 저는 우리말샘 + 표준국어대사전 + 한국어기초사전에서 총 548,385개의 단어를 추출하였습니다. 여기에는 한 글자 단어, 속담, 관용구, 구 형태의 표현은 제외하였으며, 최근 등장한 일부 신조어 역시 포함되지 않았습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;509&quot; data-start=&quot;460&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 이 단어들을 기반으로 N-gram이 어떤 방식으로 동작하는지 직접 확인해봅시다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;335&quot; data-start=&quot;271&quot; data-ke-size=&quot;size16&quot;&gt;저희는 현재 끝말잇기를 기반으로 N-gram을 이해하려고 하기 때문에, 먼저 가 ~ 힣까지 어떤 글자들이 얼마나 등장하는지 빈도수를 확인해보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;335&quot; data-start=&quot;271&quot; data-ke-size=&quot;size20&quot;&gt;1 - gram(Unigram)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcTFak/dJMcagMhJ55/12ZWmSVWz9xqMlNKSKb0y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcTFak/dJMcagMhJ55/12ZWmSVWz9xqMlNKSKb0y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcTFak/dJMcagMhJ55/12ZWmSVWz9xqMlNKSKb0y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcTFak%2FdJMcagMhJ55%2F12ZWmSVWz9xqMlNKSKb0y0%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;343&quot; height=&quot;308&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈도수를 확인해보니, 다가 포함된 단어는 전체의 약 5%를 차지하고 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvDzhY/dJMcac38fVI/oyDekKfrMfnXZ2vNZ4CP1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvDzhY/dJMcac38fVI/oyDekKfrMfnXZ2vNZ4CP1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvDzhY/dJMcac38fVI/oyDekKfrMfnXZ2vNZ4CP1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvDzhY%2FdJMcac38fVI%2FoyDekKfrMfnXZ2vNZ4CP1K%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;175&quot; height=&quot;311&quot; data-origin-width=&quot;270&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 힗, 힡과 같은 글자는 단 1회만 등장하였으며, 비율로 보면 약 0.0001% 수준에 불과하였습니다. &lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;일단 이 방식이 얼마나 효과적인지 확인하기 위해, 무작위 문자열을 생성해 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;194&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M064T/dJMb990Ihg3/J7hLMq9phWYKzygioIdio1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M064T/dJMb990Ihg3/J7hLMq9phWYKzygioIdio1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M064T/dJMb990Ihg3/J7hLMq9phWYKzygioIdio1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM064T%2FdJMb990Ihg3%2FJ7hLMq9phWYKzygioIdio1%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;194&quot; height=&quot;226&quot; data-origin-width=&quot;194&quot; data-origin-height=&quot;226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;368&quot; data-start=&quot;303&quot; data-ke-size=&quot;size16&quot;&gt;단어를&amp;nbsp;무작위로&amp;nbsp;조합해&amp;nbsp;보았지만,&amp;nbsp;사전&amp;nbsp;기준으로는&amp;nbsp;의미를&amp;nbsp;가지지&amp;nbsp;않는&amp;nbsp;문자열이&amp;nbsp;대부분이었습니다.&amp;nbsp;&lt;br /&gt;즉,&amp;nbsp;단순히&amp;nbsp;글자를&amp;nbsp;이어&amp;nbsp;붙인다고&amp;nbsp;해서&amp;nbsp;자연스러운&amp;nbsp;단어가&amp;nbsp;만들어지는&amp;nbsp;것은&amp;nbsp;아니었습니다.&amp;nbsp;그렇다면&amp;nbsp;이런&amp;nbsp;의문이&amp;nbsp;들&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;&quot;의미는&amp;nbsp;없더라도,&amp;nbsp;실제&amp;nbsp;한국어&amp;nbsp;단어에서&amp;nbsp;자주&amp;nbsp;나타나는&amp;nbsp;글자&amp;nbsp;배열을&amp;nbsp;따라가면&amp;nbsp;더&amp;nbsp;자연스러운&amp;nbsp;문자열을&amp;nbsp;만들&amp;nbsp;수&amp;nbsp;있지&amp;nbsp;않을까?&quot;&lt;br /&gt;&lt;br /&gt;이제부터는 실제 단어에서 나타나는 글자 패턴과 빈도수를 바탕으로, 무작위 문자열이 얼마나 단어처럼 보이도록 생성될 수 있는지 확인해 보겠습니다. 이거에 대한 아이디어가 바로 N-gram입니다.&lt;/p&gt;
&lt;h3 data-end=&quot;368&quot; data-start=&quot;303&quot; data-ke-size=&quot;size23&quot;&gt;2-gram (bi-gram)&lt;/h3&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;368&quot; data-start=&quot;303&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 같은 방식으로 2-gram(Bigram)으로 만들면 어떻게 변할까요?&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;159&quot; data-start=&quot;47&quot; data-ke-size=&quot;size16&quot;&gt;이제부터는 단순히 글자의 등장 횟수만 보는 것이 아니라, 현재 글자 다음에 어떤 글자가 자주 등장하는지 함께 확인하게 됩니다. 즉, 이전 글자 1개를 기반으로 다음 글자를 예측하는 방식으로 변경됩니다. 결과는 다음처럼 나왔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC46Mu/dJMcabjVHvy/BnEfUMbs225ABxo83KKwV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC46Mu/dJMcabjVHvy/BnEfUMbs225ABxo83KKwV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC46Mu/dJMcabjVHvy/BnEfUMbs225ABxo83KKwV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC46Mu%2FdJMcabjVHvy%2FBnEfUMbs225ABxo83KKwV0%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;411&quot; height=&quot;380&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;368&quot; data-start=&quot;303&quot; data-ke-size=&quot;size16&quot;&gt;'다'를 기준으로 다시 비교해보면, 기존에는 약 5%의 비율을 차지하고 있었지만 2-gram(Bigram)에서는 약 3% 수준으로 감소한 것을 확인할 수 있었습니다. 왜 이런 차이가 발생한 것일까요?&lt;/p&gt;
&lt;p data-end=&quot;184&quot; data-start=&quot;115&quot; data-ke-size=&quot;size16&quot;&gt;실제로 생성된 문자열을 확인해보니 EOS, BOS와 같은 특수 문자가 함께 포함되어 있는 것을 발견할 수 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;317&quot; data-start=&quot;186&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;254&quot; data-start=&quot;186&quot; data-section-id=&quot;1yuoc8d&quot;&gt;BOS(Beginning Of Sequence) : 문자열의 시작을 의미
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;254&quot; data-start=&quot;233&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;254&quot; data-start=&quot;233&quot; data-section-id=&quot;qqae0t&quot;&gt;예: _다, _라, _가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;317&quot; data-start=&quot;256&quot; data-section-id=&quot;euupz0&quot;&gt;EOS(End Of Sequence) : 문자열의 끝을 의미
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;317&quot; data-start=&quot;296&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;317&quot; data-start=&quot;296&quot; data-section-id=&quot;148nlct&quot;&gt;예: 다_, 라_, 가_&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;454&quot; data-start=&quot;319&quot; data-ke-size=&quot;size16&quot;&gt;즉, 2-gram부터는 단순히 글자 자체만 보는 것이 아니라, 앞뒤 위치 정보까지 함께 고려하게 됩니다. 따라서 기존에는 단순 포함 기준으로 계산되던 다의 비율이, 실제 문맥 기준으로 다시 계산되면서 비율 차이가 발생하게 된 것입니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;368&quot; data-start=&quot;303&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면, 2-gram을 이용해서 무작위 문자열을 만들어보겠습니다.&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;334&quot; data-origin-height=&quot;690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVZ4mX/dJMcaiQONlv/QGTLfEw38LmdMy5dOzUxB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVZ4mX/dJMcaiQONlv/QGTLfEw38LmdMy5dOzUxB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVZ4mX/dJMcaiQONlv/QGTLfEw38LmdMy5dOzUxB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVZ4mX%2FdJMcaiQONlv%2FQGTLfEw38LmdMy5dOzUxB0%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;204&quot; height=&quot;421&quot; data-origin-width=&quot;334&quot; data-origin-height=&quot;690&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;368&quot; data-start=&quot;303&quot; data-ke-size=&quot;size16&quot;&gt;1-gram과의 가장 큰 차이점은 시작 지점과 종료 지점이 존재한다는 점입니다. 즉, &amp;lt;BOS&amp;gt;에서 시작하여 &amp;lt;EOS&amp;gt;로 종료되는 구조를 가지게 됩니다. 아직은 왜 이런 차이가 발생하는지 직관적으로 이해되지 않을 수 있습니다. 하지만 실제로 생성된 문자열을 확인해보면, 1-gram에 비해 훨씬 실제 단어와 비슷한 형태가 등장하는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;232&quot; data-start=&quot;213&quot; data-ke-size=&quot;size16&quot;&gt;여기서 한 가지 의문이 들었습니다. &quot;왜 1-gram은 최대 길이를 6자리로 제한했을까?&quot;&lt;/p&gt;
&lt;p data-end=&quot;397&quot; data-start=&quot;267&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 너무 짧게 생성할 경우, 1-gram&amp;nbsp;역시 우연히 실제 사전에 존재하는 단어가 등장할 가능성이 높아지기 때문입니다. 즉, 모델이 자연스럽게 생성한 것인지, 단순히 짧아서 우연히 맞아떨어진 것인지 구분하기 어려워집니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;461&quot; data-start=&quot;399&quot; data-ke-size=&quot;size16&quot;&gt;따라서 조금 더 명확하게 차이를 확인하기 위해 1-gram은 최대 6자리로 고정하여 비교를 진행하였습니다. 참고로 2-gram 또한 max는 6자리입니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;461&quot; data-start=&quot;399&quot; data-ke-size=&quot;size16&quot;&gt;실제로 생성된 결과를 사전과 비교해보니, 100개의 문자열 중 27개가 실제 사전에 등재된 단어인 것을 확인할 수 있었습니다. 단순히 글자의 등장 빈도만 기반으로 생성하였음에도 생각보다 실제 단어가 꽤 많이 등장한 것입니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;211&quot; data-start=&quot;127&quot; data-ke-size=&quot;size16&quot;&gt;즉, 완전히 의미를 이해하지 못하더라도 자주 등장하는 글자 패턴만으로도 어느 정도 자연스러운 문자열이 만들어질 수 있다는 사실을 확인할 수 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;211&quot; data-start=&quot;127&quot; data-ke-size=&quot;size23&quot;&gt;정확도를 높이기 위해 gram의 차원을 높인다?&lt;/h3&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;718&quot; data-start=&quot;650&quot; data-ke-size=&quot;size16&quot;&gt;여기서 알 수 있었던 점은, gram의 차원이 높아질수록 생성 결과의 정확도 역시 함께 높아진다는 사실이었습니다. 실제로 N 값을 증가시키면 이전 문맥을 더 많이 참고하게 되기 때문에, 생성되는 문자열 역시 훨씬 자연스러워지는 경향이 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;546&quot; data-start=&quot;352&quot; data-ke-size=&quot;size16&quot;&gt;하지만 무조건 높다고 좋은 것은 아니었습니다. 차원이 지나치게 증가하게 되면 학습 데이터에 존재하지 않는 문맥에서는 제대로 동작하지 못하게 됩니다. 또한 새로운 패턴을 생성하기보다는, 학습했던 문자열 자체를 그대로 기억하는 방향에 가까워질 수 있습니다. 즉, 문맥을 일반화해서 이해한다기보다 단순히 긴 패턴을 외워버리는 형태에 가까워지는 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;563&quot; data-start=&quot;548&quot; data-ke-size=&quot;size16&quot;&gt;왜 이런 현상이 발생할까요?&lt;/p&gt;
&lt;p data-end=&quot;605&quot; data-start=&quot;565&quot; data-ke-size=&quot;size16&quot;&gt;N이 증가할수록 경우의 수 역시 함께 폭발적으로 증가하기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;637&quot; data-start=&quot;607&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같이 문맥을 기억한다고 가정해봅시다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;744&quot; data-start=&quot;639&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;661&quot; data-start=&quot;639&quot; data-section-id=&quot;1khqp2r&quot;&gt;2-gram: 하 &amp;rarr; 다&lt;/li&gt;
&lt;li data-end=&quot;687&quot; data-start=&quot;663&quot; data-section-id=&quot;141id72&quot;&gt;3-gram: 사랑 &amp;rarr; 하&lt;/li&gt;
&lt;li data-end=&quot;713&quot; data-start=&quot;689&quot; data-section-id=&quot;1m3ffng&quot;&gt;4-gram: 사랑하 &amp;rarr; 다&lt;/li&gt;
&lt;li data-end=&quot;744&quot; data-start=&quot;715&quot; data-section-id=&quot;hue8me&quot;&gt;5-gram: 사랑하다 &amp;rarr; &amp;lt;EOS&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;858&quot; data-start=&quot;746&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 N이 증가할수록 더 긴 문맥과 구체적인 패턴을 함께 참고하게 됩니다. 문제는 문맥이 지나치게 구체적이 되면, 학습 데이터에서 단 한 번도 등장하지 않은 조합들이 급격하게 많아진다는 점입니다.&lt;/p&gt;
&lt;p data-end=&quot;888&quot; data-start=&quot;860&quot; data-ke-size=&quot;size16&quot;&gt;이를 Sparsity(희소성 문제)라고 합니다.&lt;/p&gt;
&lt;p data-end=&quot;920&quot; data-start=&quot;890&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 문맥이 존재한다고 가정해봅시다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;므쀍졀&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1020&quot; data-start=&quot;951&quot; data-ke-size=&quot;size16&quot;&gt;이런 조합은 실제 학습 데이터에 존재하지 않을 가능성이 매우 높습니다. 그렇다면 모델은 다음 글자를 예측할 수 없게 됩니다.&lt;br /&gt;결국 N을 증가시키면 다음과 같은 현상이 동시에 발생하게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1091&quot; data-start=&quot;1062&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1073&quot; data-start=&quot;1062&quot; data-section-id=&quot;4qxmx8&quot;&gt;문맥 정확도 증가&lt;/li&gt;
&lt;li data-end=&quot;1091&quot; data-start=&quot;1074&quot; data-section-id=&quot;1lzn0c8&quot;&gt;하지만 데이터 커버리지 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1161&quot; data-start=&quot;1093&quot; data-ke-size=&quot;size16&quot;&gt;즉, 더 자연스러운 결과를 만들 수는 있지만, 반대로 학습 데이터에 없는 새로운 문맥에는 점점 더 약해지게 되는 것입니다. 결국 문맥을 일반화하여 예측하는 모델이라기보다, 학습 데이터를 기억하고 재현하는 방향에 가까워지게 됩니다. 그렇다면, 2-gram까지 진행해봤는데 3,4,5,6까지 진행하면서 한번 몸소 느껴봅시다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1161&quot; data-start=&quot;1093&quot; data-ke-size=&quot;size16&quot;&gt;3-gram 부터는 빠르게 진행이 되어집니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1161&quot; data-start=&quot;1093&quot; data-ke-size=&quot;size23&quot;&gt;3-gram&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uHsPT/dJMcah5vp0p/NOeC9G60YEaDhrdvHXYNqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uHsPT/dJMcah5vp0p/NOeC9G60YEaDhrdvHXYNqK/img.png&quot; data-alt=&quot;count&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uHsPT/dJMcah5vp0p/NOeC9G60YEaDhrdvHXYNqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuHsPT%2FdJMcah5vp0p%2FNOeC9G60YEaDhrdvHXYNqK%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;371&quot; height=&quot;464&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;770&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;count&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/berEpU/dJMcagrXBm8/sUHkkoKhsdveDAqKxlDKK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/berEpU/dJMcagrXBm8/sUHkkoKhsdveDAqKxlDKK1/img.png&quot; data-alt=&quot;ramdom 문자열&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/berEpU/dJMcagrXBm8/sUHkkoKhsdveDAqKxlDKK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FberEpU%2FdJMcagrXBm8%2FsUHkkoKhsdveDAqKxlDKK1%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;451&quot; height=&quot;564&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;770&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ramdom 문자열&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 단어의 길이가 줄었다는 것을 알 수 있습니다. 확률도 100개중에 71개가 실제로 사전에 존재하는 단어입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-gram&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;766&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEtYpZ/dJMcagFxqT5/OxwwQufjtjbfXdtVhvFJB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEtYpZ/dJMcagFxqT5/OxwwQufjtjbfXdtVhvFJB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEtYpZ/dJMcagFxqT5/OxwwQufjtjbfXdtVhvFJB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEtYpZ%2FdJMcagFxqT5%2FOxwwQufjtjbfXdtVhvFJB0%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;272&quot; height=&quot;324&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;766&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;648&quot; data-origin-height=&quot;836&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mIsRd/dJMcahdlv5i/N4LotKELCJujDxYq9f0bB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mIsRd/dJMcahdlv5i/N4LotKELCJujDxYq9f0bB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mIsRd/dJMcahdlv5i/N4LotKELCJujDxYq9f0bB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmIsRd%2FdJMcahdlv5i%2FN4LotKELCJujDxYq9f0bB0%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;368&quot; height=&quot;475&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;836&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-gram&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;836&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F3dHP/dJMcabxo6TT/sI1BDgkxBvqCBVFcSEQv0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F3dHP/dJMcabxo6TT/sI1BDgkxBvqCBVFcSEQv0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F3dHP/dJMcabxo6TT/sI1BDgkxBvqCBVFcSEQv0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF3dHP%2FdJMcabxo6TT%2FsI1BDgkxBvqCBVFcSEQv0K%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;342&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;836&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;554&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDBTP9/dJMcacpz2Yb/6EHM4SdKXk37XtF2M4MLE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDBTP9/dJMcacpz2Yb/6EHM4SdKXk37XtF2M4MLE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDBTP9/dJMcacpz2Yb/6EHM4SdKXk37XtF2M4MLE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDBTP9%2FdJMcacpz2Yb%2F6EHM4SdKXk37XtF2M4MLE0%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;243&quot; height=&quot;334&quot; data-origin-width=&quot;554&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;99/100&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마지막 6-gram&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rEDoX/dJMcaayxtwb/bqmFMZ5tH9JG5YHKRVVuwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rEDoX/dJMcaayxtwb/bqmFMZ5tH9JG5YHKRVVuwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rEDoX/dJMcaayxtwb/bqmFMZ5tH9JG5YHKRVVuwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrEDoX%2FdJMcaayxtwb%2FbqmFMZ5tH9JG5YHKRVVuwK%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;392&quot; height=&quot;396&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;768&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;710&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpOr5W/dJMcacpz2Yp/AYVZt0vZObILykFyRsjLU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpOr5W/dJMcacpz2Yp/AYVZt0vZObILykFyRsjLU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpOr5W/dJMcacpz2Yp/AYVZt0vZObILykFyRsjLU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpOr5W%2FdJMcacpz2Yp%2FAYVZt0vZObILykFyRsjLU1%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;482&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;770&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;놀라운 점은 5-gram과 6-gram의 확률 결과가 거의 동일하게 나타났다는 사실이었습니다. 즉, 문맥을 하나 더 늘렸음에도 생성 결과에는 큰 차이가 발생하지 않았습니다.&lt;/p&gt;
&lt;p data-end=&quot;187&quot; data-start=&quot;102&quot; data-ke-size=&quot;size16&quot;&gt;또한 이번에는 문자열 길이를 6자리로 제한하였기 때문에, 생성된 100개의 결과 중 무려 98개가 실제 사전에 등재된 단어인 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;289&quot; data-start=&quot;189&quot; data-ke-size=&quot;size16&quot;&gt;이는 N이 증가할수록 모델이 새로운 패턴을 생성하기보다, 학습 데이터에 존재했던 문자열 자체를 그대로 재현하는 방향에 가까워지고 있다는 점을 보여주는 결과라고 볼 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;289&quot; data-start=&quot;189&quot; data-ke-size=&quot;size16&quot;&gt;N-gram의 차수를 증가시킬수록 저장해야 하는 글자 조합의 수 역시 함께 증가하였습니다. 실제로 2-gram counts 파일은 약 5.6MB 수준이었지만, 6-gram counts 파일은 약 66.4MB까지 증가한 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;282&quot; data-start=&quot;149&quot; data-ke-size=&quot;size16&quot;&gt;즉, 더 긴 문맥을 참고할수록 생성 결과는 더욱 자연스러워질 수 있었지만, 반대로 저장해야 하는 패턴 수 역시 급격하게 증가하게 됩니다. 결국 문맥 정확도를 높이기 위해서는 그만큼 더 많은 저장 공간과 계산 비용이 필요하게 되는 것입니다.&lt;/p&gt;
&lt;h2 data-end=&quot;282&quot; data-start=&quot;149&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM의 예측 방식과 연결되는 개념 중 하나인 N-gram에 대해 알아보았습니다. 핵심은 이전 문맥을 기반으로 다음에 어떤 문자가 등장할지를 예측하는 구조였습니다. 실제로 N의 차원이 증가할수록 생성 결과는 더욱 자연스러워졌습니다. 하지만 반대로 문맥이 지나치게 길어질 경우, 새로운 패턴을 예측하는 것인지, 아니면 학습 데이터를 그대로 외워서 재현하는 것인지 구분하기 어려워지는 문제 역시 함께 확인할 수 있었습니다. 또한 차원이 증가할수록 경우의 수와 저장해야 하는 패턴 역시 함께 폭발적으로 증가하였고, 결국 희소성 문제(Sparsity) 역시 발생하게 되었습니다. 물론 이번 실험은 어디까지나 N-gram의 동작 원리를 이해하기 위한 단순화된 방식에 가깝습니다. 실제 LLM은 단순히 문자열 패턴만 저장하는 것이 아니라, 훨씬 복잡한 구조를 기반으로 문맥과 의미를 함께 처리합니다. 다음에는 실제 문서를 학습시켜 새로운 문장을 생성하는 방식에 대해 조금 더 깊게 확인해보겠습니다.&lt;/p&gt;</description>
      <category>개발/LLM</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/577</guid>
      <comments>https://b-programmer.tistory.com/577#entry577comment</comments>
      <pubDate>Thu, 14 May 2026 00:12:24 +0900</pubDate>
    </item>
    <item>
      <title>그레들DSL은 어떤것을 사용해야 할까?</title>
      <link>https://b-programmer.tistory.com/576</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 입사한 회사에서 새로운 프로젝트를 도입하려 하였습니다. 기존의 단일 모듈 기반 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;controller - service - repository&lt;/span&gt; 구조가 아닌 멀티 모듈 구조로 도입하기로 결정하였습니다. 멀티 모듈로 결정한 이유는 역할별 책임을 분리하여 이후 변경 범위를 줄이고 유지보수를 용이하게 만들기 위함입니다. 또한 도메인과 인프라를 분리함으로써 구조적인 의존성을 조금 더 명확하게 관리할 수 있다고 여겨졌습니다.&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;238&quot; data-ke-size=&quot;size16&quot;&gt;빌드 툴은 메이븐이 아닌 그레들로 결정하였습니다. 그레들로 결정한 이유는 확장성과 유지보수성 때문입니다. 특히 멀티 모듈 환경에서는 공통 설정 관리와 유연한 빌드 구성이 중요하다고 판단하였고, 이러한 부분에서 그레들이 조금 더 적합하다고 느껴졌습니다.&lt;/p&gt;
&lt;p data-end=&quot;470&quot; data-start=&quot;380&quot; data-ke-size=&quot;size16&quot;&gt;그레들로 결정하고 보니 메이븐처럼 XML 하나로 관리되는 방식과 달리, 그레들은 빌드 스크립트를 작성하는 방식으로 코틀린 DSL과 그루비 DSL이 존재하였습니다. 그렇다면 어떤 방식이 좋고, 어떤 기준으로 결정해야 할까요?&lt;br /&gt;&lt;br /&gt;이번 글에서는 그레들에서 제공하는 코틀린 DSL과 그루비 DSL을 비교해보며 어떤 방식이 조금 더 적절한지 고민해보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코틀린 DSL? 그루비 DSL? 왜 구분을 지을까?&lt;/h2&gt;
&lt;p data-end=&quot;399&quot; data-start=&quot;302&quot; data-ke-size=&quot;size16&quot;&gt;들어가기 앞서 그레들은 어째서 DSL을 두 가지나 지원하는지에 대해 고민해봅시다.&lt;/p&gt;
&lt;p data-end=&quot;776&quot; data-start=&quot;658&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 그루비 DSL만 존재하였습니다. 하지만 어느 순간부터 코틀린 DSL이 추가되었습니다. 그렇다면 어째서 그레들은 코틀린 DSL을 추가하였을까요? 단순히 새로운 기술이 등장했기 때문만은 아니라고 여겨집니다.&lt;/p&gt;
&lt;p data-end=&quot;881&quot; data-start=&quot;778&quot; data-ke-size=&quot;size16&quot;&gt;기존 그루비 DSL은 동적 타입 기반이다 보니 자유롭게 작성할 수 있다는 장점이 존재하였습니다. 하지만 반대로 IDE 자동완성이나 타입 안정성과 같은 부분에서는 아쉬움이 존재하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;980&quot; data-start=&quot;883&quot; data-ke-size=&quot;size16&quot;&gt;반면 코틀린 DSL은 정적 타입 기반으로 동작합니다. 그렇기 때문에 IDE 지원이나 자동완성, 컴파일 단계에서의 검증과 같은 부분에서 조금 더 안정적으로 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;1000&quot; data-start=&quot;982&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 얼마나 효과가 있을까요?&lt;/p&gt;
&lt;p data-end=&quot;1077&quot; data-start=&quot;1002&quot; data-ke-size=&quot;size16&quot;&gt;확인해보니 코틀린 DSL이 무조건적으로 좋은 것은 아니었습니다. 오히려 빌드 성능 자체는 그루비 DSL이 유리한 경우도 존재하였습니다.대표적으로 &lt;a href=&quot;https://github.com/gradle/gradle/issues/15886?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Gradle 공식 이슈&lt;/a&gt;에서는 실제 성능 비교 자료를 공개한 적이 있습니다. 해당 자료는 Gradle 8.1 기준으로 작성된 결과이며, 현재 버전과는 약간의 차이가 존재할 수 있습니다..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SZs3K/dJMcahEkGtS/N7kAUVz5Xci9OgnDzRNkpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SZs3K/dJMcahEkGtS/N7kAUVz5Xci9OgnDzRNkpK/img.png&quot; data-alt=&quot;https://github.com/gradle/gradle/issues/15886#issuecomment-1432923669&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SZs3K/dJMcahEkGtS/N7kAUVz5Xci9OgnDzRNkpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSZs3K%2FdJMcahEkGtS%2FN7kAUVz5Xci9OgnDzRNkpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;765&quot; height=&quot;249&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://github.com/gradle/gradle/issues/15886#issuecomment-1432923669&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;567&quot; data-start=&quot;479&quot; data-ke-size=&quot;size16&quot;&gt;아쉽게도 현재 최신 버전인 Gradle 9.X 기준으로 직접 비교한 공식 성능 자료는 공개되어 있지 않습니다. &lt;br /&gt;다만 Gradle 공식 자료에서는 코틀린 DSL recompilation avoidance와 관련하여 일부 상황에서 약 2.5배 정도 개선되었다고 설명하고 있습니다. (그루비 DSL와 코틀린DSL의 비교가 아닙니다. Gradle 8.2 -&amp;gt; Gradle 9.0에 대한 개선입니다.)&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1778310854088&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;Gradle | What's new in Gradle 9.0.0&quot; data-og-description=&quot;Gradle 9.0.0 is a new major release which incorporates many features since the version 8.0. It makes Gradle configuration cache the preferred execution mode, provides more clean and actionable error reporting, and also updates Gradle to Kotlin 2 and Groovy&quot; data-og-host=&quot;gradle.org&quot; data-og-source-url=&quot;https://gradle.org/whats-new/gradle-9/?utm_source=chatgpt.com&quot; data-og-url=&quot;https://gradle.org/whats-new/gradle-9/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oyViS/dJMb82eRlQI/YoZERpowvMUghf7clx06Vk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/b5Qg4I/dJMb9aKJo3G/YZkhKtBfAUYBhMsWk5P1F0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cKWii5/dJMb81G1Ilr/6Yk1gKPJ2WKCUMrKc0BiqK/img.png?width=1803&amp;amp;height=802&amp;amp;face=1397_304_1574_481&quot;&gt;&lt;a href=&quot;https://gradle.org/whats-new/gradle-9/?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gradle.org/whats-new/gradle-9/?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oyViS/dJMb82eRlQI/YoZERpowvMUghf7clx06Vk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/b5Qg4I/dJMb9aKJo3G/YZkhKtBfAUYBhMsWk5P1F0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cKWii5/dJMb81G1Ilr/6Yk1gKPJ2WKCUMrKc0BiqK/img.png?width=1803&amp;amp;height=802&amp;amp;face=1397_304_1574_481');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Gradle | What's new in Gradle 9.0.0&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Gradle 9.0.0 is a new major release which incorporates many features since the version 8.0. It makes Gradle configuration cache the preferred execution mode, provides more clean and actionable error reporting, and also updates Gradle to Kotlin 2 and Groovy&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gradle.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;227&quot; data-start=&quot;103&quot; data-ke-size=&quot;size16&quot;&gt;그럼에도 불구하고 현재도 순수 빌드 성능만 보면 상황에 따라 그루비 DSL이 조금 더 유리한 경우가 존재한다고 합니다.&lt;/p&gt;
&lt;p data-end=&quot;104&quot; data-start=&quot;68&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 어째서 많은 기업들이 코틀린 DSL을 선택하는 것일까요?&lt;/p&gt;
&lt;p data-end=&quot;224&quot; data-start=&quot;106&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로 IDE 자동완성, 타입 안정성, 리팩토링 안정성, 대규모 멀티 모듈 유지보수와 같은 부분 때문이라고 합니다. 실제로 Gradle 역시 8.2 버전부터 코틀린 DSL을 기본 생성 방식으로 변경하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;238&quot; data-start=&quot;226&quot; data-ke-size=&quot;size16&quot;&gt;즉 핵심은 단순합니다.&lt;/p&gt;
&lt;p data-end=&quot;321&quot; data-start=&quot;240&quot; data-ke-size=&quot;size16&quot;&gt;코틀린 DSL은 빌드 성능을 극적으로 개선하기 위한 목적보다는, 조금 더 안정적이고 유지보수하기 쉬운 빌드 스크립트를 만들기 위해 등장하였습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;434&quot; data-start=&quot;323&quot; data-ke-size=&quot;size16&quot;&gt;정리하자면, 상황에 따라 빌드 성능은 그루비 DSL이 유리한 경우도 존재하였습니다. 반면 코틀린 DSL은 IDE 지원과 타입 안정성을 기반으로 유지보수에 조금 더 초점을 맞춘 방식이라고 느껴졌습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;227&quot; data-start=&quot;103&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그렇다면 실제로는 어떤 차이가 존재할까요?&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;개념적으로 보면 유지보수성과 타입 안정성이라는 차이로 정리할 수 있었습니다. 하지만 단순히 설명만으로는 크게 와닿지 않았습니다. 아무래도 실제 코드로 확인하지 못했기 때문이라고 여겨졌습니다. &lt;/span&gt;&lt;span&gt;이번에는 조금 더 문법적인 부분에서 차이를 확인해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;227&quot; data-start=&quot;103&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;문법적인 차이는?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 그루비 DSL과 코틀린 DSL을 비교하였을 때, &lt;u&gt;자동완성, 타입 안정성, 리팩토링 안정성, 대규모 멀티 모듈 유지보수&lt;/u&gt;와 같은 부분들이 코틀린 DSL의 강점이라고 하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;218&quot; data-start=&quot;102&quot; data-ke-size=&quot;size16&quot;&gt;하지만 자동완성의 경우에는 IDE의 발전으로 인해, 이제는 코틀린 DSL만의 강점이라고 말하기에는 애매하다고 느껴졌습니다. &lt;br /&gt;실제로 그루비 DSL 역시 IDE에서 자동완성을 어느 정도 지원하고 있기 때문입니다. &lt;br /&gt;그렇다면 코틀린 DSL의 장점은 어디에서 더 명확하게 드러날까요?&lt;/p&gt;
&lt;p data-end=&quot;218&quot; data-start=&quot;102&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로는 타입 안정성, 리팩토링 안정성, 그리고 대규모 멀티 모듈 유지보수 측면에서 차이가 조금 더 드러난다고 느껴졌습니다.&lt;/p&gt;
&lt;p data-end=&quot;433&quot; data-start=&quot;331&quot; data-ke-size=&quot;size16&quot;&gt;그루비 DSL은 동적 타입 기반이기 때문에 유연하게 작성할 수 있다는 장점이 존재합니다. 하지만 그만큼 잘못된 설정이 작성되었을 때 실행 시점에서 확인되는 경우가 존재할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;433&quot; data-start=&quot;331&quot; data-ke-size=&quot;size16&quot;&gt;반면 코틀린 DSL은 정적 타입 기반으로 동작하기 때문에, 잘못된 설정을 작성했을 때 IDE나 컴파일 단계에서 더 빠르게 확인할 수 있습니다. 또한 타입 기반으로 코드 탐색이나 리팩토링이 가능하기 때문에, 모듈 수가 많아지고 설정이 복잡해질수록 유지보수 측면에서 조금 더 안정적으로 사용할 수 있다고 여겨졌습니다.&lt;/p&gt;
&lt;h4 data-end=&quot;433&quot; data-start=&quot;331&quot; data-ke-size=&quot;size20&quot;&gt;코틀린 DSL의 타입 안정성&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;플러그인 선언&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그루비 DSL은 플러그인을 지정할 때 id를 이용하여 문자열 형태로 등록합니다.&amp;nbsp;&lt;br /&gt;반면 코틀린 DSL은 다음과 같이 바로 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6dm1d/dJMcagSZUKX/YCiIJMKphegGu5c9eum5uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6dm1d/dJMcagSZUKX/YCiIJMKphegGu5c9eum5uK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6dm1d/dJMcagSZUKX/YCiIJMKphegGu5c9eum5uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6dm1d%2FdJMcagSZUKX%2FYCiIJMKphegGu5c9eum5uK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;679&quot; height=&quot;397&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5&quot; data-message-id=&quot;d0251abc-a386-42be-ae55-a96c81f64790&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-end=&quot;259&quot; data-start=&quot;172&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 문법 차이처럼 보였습니다. 하지만 자세히 보면 코틀린 DSL의 java는 단순 문자열이 아니라 타입 안전한 accessor로 동작합니다. 즉 문자열 기반으로 처리되는 것이 아니라, IDE와 Gradle이 인식할 수 있는 형태로 제공된다는 차이가 존재하였습니다. &lt;br /&gt;그렇기 때문에 잘못된 값을 사용하거나 존재하지 않는 설정을 작성했을 때 IDE나 컴파일 단계에서 조금 더 빠르게 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;java toolChain&lt;/h4&gt;
&lt;div&gt;
&lt;p data-end=&quot;49&quot; data-start=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;이 부분에서 코틀린 DSL의 장점이 드러난다고 합니다. 그루비 DSL에서는 다음과 같이 설정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br18zW/dJMcadBSbKa/JkjfRw12k6GSXlX2Nd1oI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br18zW/dJMcadBSbKa/JkjfRw12k6GSXlX2Nd1oI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br18zW/dJMcadBSbKa/JkjfRw12k6GSXlX2Nd1oI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr18zW%2FdJMcadBSbKa%2FJkjfRw12k6GSXlX2Nd1oI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;611&quot; height=&quot;184&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 코틀린 DSL은 다음과 같이 작성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xAZy4/dJMcajvoieO/HKovJEwg6lcSk6vvHmal0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xAZy4/dJMcajvoieO/HKovJEwg6lcSk6vvHmal0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xAZy4/dJMcajvoieO/HKovJEwg6lcSk6vvHmal0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxAZy4%2FdJMcajvoieO%2FHKovJEwg6lcSk6vvHmal0K%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;711&quot; height=&quot;162&quot; data-origin-width=&quot;1122&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 보았을 때는 오히려 코틀린 DSL쪽이 더 복잡하게 느껴졌습니다. set()과 같은 코드가 추가되다 보니 뭔가 덕지덕지 붙은 느낌도 존재하였습니다. 그렇다면 어째서 이것이 코틀린 DSL의 장점이라고 말하는 것일까요?&lt;/p&gt;
&lt;p data-end=&quot;577&quot; data-start=&quot;515&quot; data-ke-size=&quot;size16&quot;&gt;가독성만 보면 오히려 그루비 DSL이 더 좋아 보였습니다. 하지만 차이는 값을 어떻게 다루는지에 존재하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;704&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;그루비 DSL은 동적 타입 기반이다 보니 비교적 자유롭게 값을 설정할 수 있습니다. 반면 코틀린 DSL은 languageVersion이 어떤 타입인지 알고 있으며, 정해진 API를 통해 값을 설정하도록 구성되어 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;772&quot; data-start=&quot;706&quot; data-ke-size=&quot;size16&quot;&gt;즉 단순히 문법이 길어진 것이 아니라, 그레들 API를 타입 기반으로 조금 더 안전하게 다루도록 변경된 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mOhbs/dJMcagZIZ9W/SQXbjEYUe43xfX1KtodUDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mOhbs/dJMcagZIZ9W/SQXbjEYUe43xfX1KtodUDk/img.png&quot; data-alt=&quot;gradle api&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mOhbs/dJMcagZIZ9W/SQXbjEYUe43xfX1KtodUDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmOhbs%2FdJMcagZIZ9W%2FSQXbjEYUe43xfX1KtodUDk%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;1480&quot; height=&quot;382&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;gradle api&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5&quot; data-message-id=&quot;85f8c42c-6a2d-4752-8799-848c7d9201e0&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;81&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;즉, 코틀린 DSL은 단순 문자열 기반으로 동작하는 것이 아니라, Gradle API를 타입 기반으로 직접 활용하고 있다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Task&amp;nbsp;타입&amp;nbsp;지정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간략하게 task가 어떤것인지 부터 이해를 해봅시다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;104&quot; data-start=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;그레들에서 Task는 하나의 작업 단위를 의미합니다. 쉽게 말해 Gradle이 수행하는 기능 하나하나를 Task라고 볼 수 있습니다.&lt;br /&gt;예를 들어 프로젝트 컴파일, 테스트 실행, JAR 생성, 애플리케이션 실행등과 같은 작업들이 모두 Task로 동작합니다. &lt;br /&gt;실제로 build 역시 내부적으로 여러 Task를 실행하면서 동작합니다.&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, 그레들은 단순히 코드를 빌드하는 도구가 아니라, 여러 작업(Task)을 조합하여 실행하는 구조라고 볼 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;p data-end=&quot;80&quot; data-start=&quot;56&quot; data-ke-size=&quot;size16&quot;&gt;그루비 DSL에서는 다음과 같이 작성합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pJWTk/dJMcaaSM2cS/jli9d448dOha8TKRKHou40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pJWTk/dJMcaaSM2cS/jli9d448dOha8TKRKHou40/img.png&quot; data-alt=&quot;테스트 실행 환경 공통 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pJWTk/dJMcaaSM2cS/jli9d448dOha8TKRKHou40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpJWTk%2FdJMcaaSM2cS%2Fjli9d448dOha8TKRKHou40%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;522&quot; height=&quot;303&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트 실행 환경 공통 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;196&quot; data-start=&quot;171&quot; data-ke-size=&quot;size16&quot;&gt;반면 코틀린 DSL은 다음과 같이 작성합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwd9sm/dJMcaf0ONKW/VY8195ApsIyspHOdMHJ70k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwd9sm/dJMcaf0ONKW/VY8195ApsIyspHOdMHJ70k/img.png&quot; data-alt=&quot;테스트 실행 환경 공통 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwd9sm/dJMcaf0ONKW/VY8195ApsIyspHOdMHJ70k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwd9sm%2FdJMcaf0ONKW%2FVY8195ApsIyspHOdMHJ70k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;577&quot; height=&quot;263&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트 실행 환경 공통 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;392&quot; data-start=&quot;306&quot; data-ke-size=&quot;size16&quot;&gt;처음 보았을 때는 단순히 문법 차이처럼 느껴졌습니다. 하지만 코틀린 DSL은 Test 타입의 task만 대상으로 지정하고 있다는 차이가 존재하였습니다. 즉, Gradle이 현재 블록이 Test 타입이라는 것을 명확하게 알고 있기 때문에, 해당 타입에서 사용할 수 있는 API들을 IDE가 조금 더 안정적으로 인식할 수 있었습니다. &lt;br /&gt;결국 코틀린 DSL은 단순히 문법이 다른 것이 아니라, Gradle API를 명확한 타입 기반으로 직접 다루고 있다는 특징이 존재하였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;의존성 선언은 타입 안정성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 다른 분들과 달리 타입 안정성이 떨어지는 편입니다. 왜냐하면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1772&quot; data-origin-height=&quot;1096&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6xjys/dJMcahK8KtJ/Mj7GdEpnS7k74JqUKE0511/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6xjys/dJMcahK8KtJ/Mj7GdEpnS7k74JqUKE0511/img.png&quot; data-alt=&quot;각기 다른 프로젝트입니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6xjys/dJMcahK8KtJ/Mj7GdEpnS7k74JqUKE0511/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6xjys%2FdJMcahK8KtJ%2FMj7GdEpnS7k74JqUKE0511%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;666&quot; height=&quot;412&quot; data-origin-width=&quot;1772&quot; data-origin-height=&quot;1096&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;각기 다른 프로젝트입니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여전히 문자열 기반으로 의존성을 선언하고 있다는 점도 확인할 수 있었습니다. 그렇다는 것은 의존성 이름이나 버전에 오타가 발생하더라도 컴파일 단계에서 바로 확인되는 것이 아니라, 의존성을 해석하는 시점에서 실패할 수 있다는 의미입니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;243&quot; data-start=&quot;133&quot; data-ke-size=&quot;size16&quot;&gt;물론 이를 보완하기 위한 다양한 방법들도 존재하였습니다. 하지만 이번 글은 코틀린 DSL과 그루비 DSL의 차이를 확인하는 것이 목적이기 때문에, 해당 내용은 여기까지만 보고 넘어가도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코틀린 DSL이 타입 안정성 측면에서 어떤 장점이 있는지 살펴보았습니다. 그렇다면 대규모 멀티 모듈 유지보수 측면에서는 어떤 점이 더 좋을까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대규모 멀티 모듈 유지보수 측면에서는 어떤점이 더 좋을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 그루비 DSL은 동적 타입 구조라고 설명하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fl1wF/dJMcahElaCV/KFuWc9xWpxJkwGlTskz2Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fl1wF/dJMcahElaCV/KFuWc9xWpxJkwGlTskz2Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fl1wF/dJMcahElaCV/KFuWc9xWpxJkwGlTskz2Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFl1wF%2FdJMcahElaCV%2FKFuWc9xWpxJkwGlTskz2Kk%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;624&quot; height=&quot;188&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반면 코틀린 DSL은 정적 타입 기반으로 동작합니다. 즉, Gradle 설정을 타입 기반으로 관리할 수 있다는 의미입니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그렇기 때문에 프로젝트 규모가 커지고 멀티 모듈 환경처럼 공통 설정과 build logic이 증가할수록, IDE 지원이나 리팩토링, 설정 관리 측면에서 조금 더 안정적으로 유지보수할 수 있다고 여겨졌습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5&quot; data-message-id=&quot;1d9ec0c6-acfb-4fcb-bd22-a639e60716d0&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-end=&quot;203&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;하지만 타입이 정적이라고 해서 대규모 프로젝트에 무조건 적합하다고 보기는 어려워 보였습니다. 언어 관점에서 보면 일반적으로 동적 타입 언어보다 정적 타입 언어가 대규모 환경에서 유지보수 측면에 조금 더 유리하다고 알려져 있습니다. 컴파일 단계에서 타입 오류를 조금 더 빠르게 확인할 수 있고, IDE 지원이나 리팩토링 역시 조금 더 안정적으로 동작하기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;275&quot; data-start=&quot;205&quot; data-ke-size=&quot;size16&quot;&gt;실제로 Kotlin 공식 문서에서도 정적 타입 기반 구조가 IDE 리팩토링과 자동완성 지원에 도움을 준다고 설명하고 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1778413229257&quot; style=&quot;color: #333333; text-align: start;&quot; contenteditable=&quot;false&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lWNOW/dJMb9efiqsA/1k2rTqSKO5PCHDuopAFcFK/img.png?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800&quot; data-og-url=&quot;https://kotlinlang.org/docs/gradle-best-practices.html&quot; data-og-source-url=&quot;https://kotlinlang.org/docs/gradle-best-practices.html#use-a-version-catalog&quot; data-og-host=&quot;kotlinlang.org&quot; data-og-description=&quot;&quot; data-og-title=&quot;Gradle best practices | Kotlin&quot; data-og-type=&quot;website&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://kotlinlang.org/docs/gradle-best-practices.html#use-a-version-catalog&quot; data-source-url=&quot;https://kotlinlang.org/docs/gradle-best-practices.html#use-a-version-catalog&quot;&gt;&lt;br /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;Gradle best practices | Kotlin&lt;/p&gt;
&lt;p style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;kotlinlang.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;br /&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bduScW/dJMcab5dmm0/D1m6rWOIFsgRjBpStPE1M0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bduScW/dJMcab5dmm0/D1m6rWOIFsgRjBpStPE1M0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bduScW/dJMcab5dmm0/D1m6rWOIFsgRjBpStPE1M0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbduScW%2FdJMcab5dmm0%2FD1m6rWOIFsgRjBpStPE1M0%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;728&quot; height=&quot;165&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-end=&quot;486&quot; data-start=&quot;388&quot; data-ke-size=&quot;size16&quot;&gt;또한 Gradle 공식 문서에서는 Kotlin DSL이 기존 그루비 DSL의 동적 접근 방식 대신, type-safe model accessor를 제공한다고 설명하고 있습니다.&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;figure id=&quot;og_1778413225914&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;Gradle best practices | Kotlin&quot; data-og-description=&quot; &quot; data-og-host=&quot;kotlinlang.org&quot; data-og-source-url=&quot;https://kotlinlang.org/docs/gradle-best-practices.html#use-a-version-catalog&quot; data-og-url=&quot;https://kotlinlang.org/docs/gradle-best-practices.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lWNOW/dJMb9efiqsA/1k2rTqSKO5PCHDuopAFcFK/img.png?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800&quot;&gt;&lt;a href=&quot;https://kotlinlang.org/docs/gradle-best-practices.html#use-a-version-catalog&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kotlinlang.org/docs/gradle-best-practices.html#use-a-version-catalog&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lWNOW/dJMb9efiqsA/1k2rTqSKO5PCHDuopAFcFK/img.png?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Gradle best practices | Kotlin&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kotlinlang.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-end=&quot;919&quot; data-start=&quot;818&quot; data-ke-size=&quot;size16&quot;&gt;다만, 이것이 빌드 툴에서도 완전히 동일하게 적용되는지는 아직 확실하지 않았습니다. 실제로 빌드 성능만 보면 상황에 따라 그루비 DSL이 조금 더 유리한 경우도 존재하였기 때문입니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1053&quot; data-start=&quot;921&quot; data-ke-size=&quot;size16&quot;&gt;또한 실제 대규모 멀티 모듈 환경에서도 여전히 &lt;a href=&quot;https://www.reddit.com/r/androiddev/comments/1ehmbtn/clean_gradle_for_a_multimodule_project_groovy_vs/?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;그루비 DSL을 사용하는 사례&lt;/a&gt;들 역시 존재하였습니다. 결국 어떤 방식이 무조건적으로 정답이라기보다는, 프로젝트 규모나 팀의 경험, 유지보수 방향에 따라 선택이 달라질 수 있다고 여겨졌습니다.&lt;/p&gt;
&lt;p data-end=&quot;123&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;아쉽게도 어떤 방식을 더 선호하는지에 대한 명확한 통계 자료는 찾아볼 수 없었습니다. 하지만 여러 사례들을 종합해보면, 코틀린 DSL을 선호하는 입장과 그루비 DSL을 선호하는 입장 두 가지 모두를 확인할 수 있었습니다.&lt;br /&gt;실제로 Gradle 공식 블로그에서는 신규 프로젝트에 대해 Kotlin DSL을 기본 생성 방식으로 변경하였다고 설명하고 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1778413707669&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;Kotlin DSL is Now the Default for New Gradle Builds&quot; data-og-description=&quot;Kotlin DSL is Now the Default for New Gradle Builds Calendar April 13, 2023 Introduction Kotlin DSL for Gradle was introduced in version 3.0 of the Gradle Build Tool in August 2016 and released as 1.0 in Gradle 5.0. Since then, it&amp;rsquo;s been growing in popul&quot; data-og-host=&quot;blog.gradle.org&quot; data-og-source-url=&quot;https://blog.gradle.org/kotlin-dsl-is-now-the-default-for-new-gradle-builds?utm_source=chatgpt.com&quot; data-og-url=&quot;https://blog.gradle.org/kotlin-dsl-is-now-the-default-for-new-gradle-builds?utm_source=chatgpt.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RGch0/dJMb8XkjQVg/2z5ePU7Lbh5eo9SwdcPh50/img.png?width=4209&amp;amp;height=1253&amp;amp;face=0_0_4209_1253&quot;&gt;&lt;a href=&quot;https://blog.gradle.org/kotlin-dsl-is-now-the-default-for-new-gradle-builds?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.gradle.org/kotlin-dsl-is-now-the-default-for-new-gradle-builds?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RGch0/dJMb8XkjQVg/2z5ePU7Lbh5eo9SwdcPh50/img.png?width=4209&amp;amp;height=1253&amp;amp;face=0_0_4209_1253');&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;Kotlin DSL is Now the Default for New Gradle Builds&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Kotlin DSL is Now the Default for New Gradle Builds Calendar April 13, 2023 Introduction Kotlin DSL for Gradle was introduced in version 3.0 of the Gradle Build Tool in August 2016 and released as 1.0 in Gradle 5.0. Since then, it&amp;rsquo;s been growing in popul&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.gradle.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-end=&quot;467&quot; data-start=&quot;355&quot; data-ke-size=&quot;size16&quot;&gt;반면 복잡한 build logic을 가진 대규모 레거시 프로젝트에서는 마이그레이션 비용이나 기존 생태계 문제로 인해, 여전히 그루비 DSL을 유지하는 경우도 존재한다는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;596&quot; data-start=&quot;469&quot; data-ke-size=&quot;size16&quot;&gt;즉 실제 현업에서는 단순히 &quot;무조건 코틀린 DSL이 좋다&quot;기보다는, 신규 프로젝트에서는 코틀린 DSL을 선택하는 흐름이 증가하고 있었고, 대규모 레거시 프로젝트에서는 여전히 그루비 DSL을 유지하는 사례들도 존재하였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;거두 절미하고 빌드 툴을 사용하는 이유가 무엇일까?&lt;/h2&gt;
&lt;p data-end=&quot;164&quot; data-start=&quot;51&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로 외부 라이브러리나 프레임워크를 가져오고, 프로젝트를 빌드하고 실행하기 위함입니다. 결국 build.gradle 역시 의존성을 관리하고 프로젝트를 실행하기 위한 설정 파일이라고 볼 수 있습니다. 그렇다면 외부에서 정보를 가져오는 작업에서도 정적인지 동적인지가 그렇게까지 중요한 것일까요?&lt;/p&gt;
&lt;p data-end=&quot;298&quot; data-start=&quot;219&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로는 이 부분에서 조금 의문이 들었습니다. 실제로 의존성 선언만 보더라도 여전히 문자열 기반으로 작성되는 부분이 존재하였기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;443&quot; data-start=&quot;394&quot; data-ke-size=&quot;size16&quot;&gt;즉 코틀린 DSL이라고 해서 모든 부분이 완전한 타입 안정성을 가지는 것은 아니었습니다.&lt;/p&gt;
&lt;p data-end=&quot;539&quot; data-start=&quot;445&quot; data-ke-size=&quot;size16&quot;&gt;오히려 실제 차이는 단순 의존성 선언보다는, 공통 build logic이나 task 설정, 멀티 모듈 환경에서의 설정 관리 측면에서 조금 더 드러나는 것으로 보였습니다. 결국, 빌드 툴 관점에서는 단순히 정적 타입인지 동적 타입인지 자체보다, 프로젝트 규모가 커졌을 때 build logic을 얼마나 안정적으로 관리할 수 있는지가 더 중요한 부분처럼 느껴졌습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;539&quot; data-start=&quot;445&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확하게 어떤 DSL을 사용하는 것이 무조건 더 좋다는 내용은 찾을 수 없었습니다. 다만 여러 자료들을 확인해보면서, 단순히 문법 차이가 아니라 어떤 방향으로 build logic을 관리할 것인지에 대한 고민이 담겨있다는 것을 느낄 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;240&quot; data-start=&quot;148&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 그루비 DSL만 사용하였습니다. 그러다 코틀린 DSL 역시 알게 되었고, 어떤 DSL을 사용하는 것이 더 적절한지에 대해 여러 자료들을 찾아보게 되었습니다.&lt;/p&gt;
&lt;p data-end=&quot;402&quot; data-start=&quot;242&quot; data-ke-size=&quot;size16&quot;&gt;실제로 현재 흐름을 보면 신규 프로젝트에서는 코틀린 DSL을 사용하는 사례가 증가하고 있었고, 레거시 프로젝트에서는 여전히 그루비 DSL을 유지하는 경우도 존재하였습니다. 하지만 이 역시 프로젝트 규모나 팀 경험, migration 비용 등에 따라 선택이 달라질 수 있다고 느껴졌습니다.&lt;/p&gt;
&lt;p data-end=&quot;549&quot; data-start=&quot;404&quot; data-ke-size=&quot;size16&quot;&gt;또한 성능만 보면 상황에 따라 그루비 DSL이 조금 더 유리한 경우도 존재하였습니다. 반면 코틀린 DSL은 정적 타입 기반 구조를 통해 IDE 지원이나 리팩토링, build logic 관리 측면에서 조금 더 안정적으로 사용할 수 있다는 평가들이 존재하였습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;684&quot; data-start=&quot;551&quot; data-ke-size=&quot;size16&quot;&gt;결국 어떤 DSL이 절대적으로 더 좋다기보다는, 프로젝트 환경과 유지보수 방향에 따라 선택이 달라질 수 있다고 여겨졌습니다. 다만 신규 프로젝트를 시작하는 입장이라면, 개인적으로는 코틀린 DSL을 사용하는 방향이 조금 더 좋아 보였습니다.&lt;/p&gt;</description>
      <category>개발/이력관리</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/576</guid>
      <comments>https://b-programmer.tistory.com/576#entry576comment</comments>
      <pubDate>Sun, 10 May 2026 21:19:36 +0900</pubDate>
    </item>
    <item>
      <title>배치 작업시 예외가 발생한다면 어떻게 처리할까?</title>
      <link>https://b-programmer.tistory.com/575</link>
      <description>&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-id=&quot;0a67ee59-3609-4c34-a5ea-b86611239561&quot; data-message-author-role=&quot;user&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;만약 외부 API의 스펙이 변경된다면 어떻게 처리해야 할까요? 이 경우에도 skip을 해야 할까요, 아니면 retry를 해야 할까요?&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5-thinking&quot; data-message-id=&quot;bddb2a5a-a1fa-42a4-9cf7-172f8ab25695&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-end=&quot;196&quot; data-start=&quot;76&quot; data-ke-size=&quot;size16&quot;&gt;외부 API 장애라고 해서 모두 같은 방식으로 처리하면 안 됩니다. 예를 들어 특정 row의 값이 잘못되었거나 필수 데이터가 누락된 경우라면, 이는 개별 데이터 품질 문제로 보고 skip 처리를 고려할 수 있습니다. 반면 외부 API의 응답 구조가 변경된 경우는 다르게 봐야 합니다. 이는 단순히 특정 데이터 하나가 잘못된 것이 아니라, 배치가 기대하던 계약이나 제약 자체가 깨진 상황입니다. 이런 경우까지 skip해버리면 전체 데이터가 잘못 처리되거나, 문제를 늦게 발견할 수 있습니다. 또한 네트워크 타임아웃이나 5XX 오류처럼 일시적인 장애는 retry 대상이 될 수 있습니다. 하지만 API 스펙 변경은 retry를 한다고 해결되는 문제가 아닙니다. 구조 자체가 달라졌기 때문입니다. 따라서 응답 구조 변경이 감지되면 skip이나 retry보다는 배치를 실패시키고, 빠르게 문제를 감지할 수 있도록 처리하는 것이 더 안전합니다.&lt;/p&gt;
&lt;h2 data-end=&quot;196&quot; data-start=&quot;76&quot; data-ke-size=&quot;size26&quot;&gt;어째서 process단계에서 skip or retry를 하는가?&lt;/h2&gt;
&lt;p data-end=&quot;60&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;배치 작업은 크게 읽기(Read) &amp;rarr; 처리(Process) &amp;rarr; 쓰기(Write) 단계로 구분할 수 있습니다. 이 중에서 skip이나 retry 같은 예외 처리 전략이 가장 중요하게 적용되는 구간은 처리(Process) 단계입니다. 그 이유는 단순합니다. 데이터를 읽거나 저장하는 것도 중요하지만, &lt;u&gt;실제 비즈니스 로직과 데이터 변환이 수행되는 중심 영역이 바로 처리 단계&lt;/u&gt;이기 때문입니다. 예를 들어 특정 row의 데이터가 잘못되었거나 필수 값이 누락된 경우에는 처리 단계에서 이를 검증하고 skip 여부를 결정할 수 있습니다. 또한 일시적인 네트워크 장애나 외부 API 호출 실패가 발생했다면 retry 전략 역시 이 과정에서 고려될 수 있습니다. 반면 외부 API의 응답 구조 자체가 변경된 경우는 다르게 봐야 합니다. 이는 단순 데이터 오류가 아니라, 처리 로직이 기대하던 계약 자체가 깨진 상황입니다. 이런 경우까지 skip하거나 무의미하게 retry를 반복하면 문제를 숨긴 채 잘못된 데이터가 계속 처리될 수 있습니다. 따라서 처리 단계에서는 예외를 단순히 &quot;실패&quot;로만 보는 것이 아니라, 어떤 종류의 실패인지 구분하여 skip, retry, fail-fast 전략을 다르게 가져가는 것이 중요합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;60&quot; data-start=&quot;0&quot; data-ke-size=&quot;size23&quot;&gt;먼저 커스텀 exception을 준비해줍니다.&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRtfPG/dJMcagFruAX/on9Dw5ICgkPJHDNtTVfYHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRtfPG/dJMcagFruAX/on9Dw5ICgkPJHDNtTVfYHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRtfPG/dJMcagFruAX/on9Dw5ICgkPJHDNtTVfYHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRtfPG%2FdJMcagFruAX%2Fon9Dw5ICgkPJHDNtTVfYHk%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;1450&quot; height=&quot;290&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-end=&quot;85&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;사실 커스텀 Exception은 반드시 만들어야 하는 것은 아닙니다. 기존 자바에서 제공하는 Exception만으로도 충분히 예외 처리는 가능합니다. 그럼에도 불구하고 커스텀 Exception을 만드는 이유는 단순합니다. 예외가 발생했을 때 상황을 더 명확하게 구분하고, 보다 간단하게 처리하기 위함입니다. 예를 들어 IllegalArgumentException, RuntimeException만 사용하게 되면 현재 발생한 문제가 비즈니스 오류인지, 외부 API 문제인지, 일시적인 장애인지 구분하기 어려워질 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;345&quot; data-start=&quot;300&quot; data-ke-size=&quot;size16&quot;&gt;반면 커스텀 Exception을 사용하면 예외 자체에 의미를 부여할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;432&quot; data-start=&quot;347&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;371&quot; data-start=&quot;347&quot; data-section-id=&quot;1s4bvwm&quot;&gt;InvalidCouponException&lt;/li&gt;
&lt;li data-end=&quot;401&quot; data-start=&quot;372&quot; data-section-id=&quot;1uff0kh&quot;&gt;ExternalApiTimeoutException&lt;/li&gt;
&lt;li data-end=&quot;432&quot; data-start=&quot;402&quot; data-section-id=&quot;1lixupn&quot;&gt;ResponseSpecChangedException&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;497&quot; data-start=&quot;434&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 예외를 목적에 따라 분리하면 catch 구문이나 예외 처리 정책에서도 의도를 명확하게 표현할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;512&quot; data-start=&quot;499&quot; data-ke-size=&quot;size16&quot;&gt;결국 핵심은 단순합니다. 커스텀 Exception의 목적은 &quot;새로운 기능 추가&quot;가 아니라, &lt;br /&gt;예외 상황을 명확하게 분리하고 처리 흐름을 단순하게 만들기 위함입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pzad1/dJMcajowpqb/w8cgOYisRnBYajuujV4yTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pzad1/dJMcajowpqb/w8cgOYisRnBYajuujV4yTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pzad1/dJMcajowpqb/w8cgOYisRnBYajuujV4yTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpzad1%2FdJMcajowpqb%2Fw8cgOYisRnBYajuujV4yTk%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;575&quot; height=&quot;298&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 커스텀 Exception을 사용하지 않는다면, 예외를 구분하기 위한 조건문이 많아지면서 처리 로직이 복잡해질 수 있습니다.&lt;br /&gt;결과적으로 catch 구문 내부가 점점 비대해지고, 코드 역시 읽기 어려워질 가능성이 높아집니다. 예를 들어 메시지 문자열이나 상태값으로 예외를 구분하기 시작하면, 예외 처리의 책임이 분산되고 유지보수도 어려워질 수 있습니다. 반면 커스텀 Exception을 사용하면 예외 자체에 의미를 담을 수 있기 때문에, 어떤 상황에서 발생한 문제인지 명확하게 표현할 수 있습니다. 또한 예외 타입 기준으로 처리 전략을 나눌 수 있어 코드 흐름도 훨씬 단순해집니다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 핵심은 단순합니다. 커스텀 Exception은 단순히 예외 클래스를 늘리는 것이 아니라, 복잡한 예외 처리 로직을 역할별로 분리하여 코드의 가독성과 유지보수성을 높이기 위한 방법입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그러면 어떤 정보를 skip할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 커스텀 Exception을 만들었다고 해서 비즈니스 로직이 사라지는 것은 아니라는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPlhHC/dJMcadIDVYw/7vX5pC5Wpc6KFhRDxIc1XK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPlhHC/dJMcadIDVYw/7vX5pC5Wpc6KFhRDxIc1XK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPlhHC/dJMcadIDVYw/7vX5pC5Wpc6KFhRDxIc1XK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPlhHC%2FdJMcadIDVYw%2F7vX5pC5Wpc6KFhRDxIc1XK%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;541&quot; height=&quot;341&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 skip 여부를 결정하는 기준은 Exception 클래스가 아니라, 비즈니스적으로 &quot;이 데이터를 버려도 되는가&quot;에 대한 판단입니다.&lt;br /&gt;예를 들어 특정 row의 필수 값이 누락되었거나, 이미 잘못된 데이터라고 판단되는 경우에는 해당 데이터만 skip하고 다음 작업을 이어갈 수 있습니다. 이런 경우는 개별 데이터 문제이기 때문에 전체 배치를 중단할 필요는 없을 수 있습니다.&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;1474&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTHzsp/dJMcafftAFm/wQ7IHf07QeVgddDGjYOPV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTHzsp/dJMcafftAFm/wQ7IHf07QeVgddDGjYOPV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTHzsp/dJMcafftAFm/wQ7IHf07QeVgddDGjYOPV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTHzsp%2FdJMcafftAFm%2FwQ7IHf07QeVgddDGjYOPV0%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;522&quot; height=&quot;196&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우는 해당 row 자체의 문제이기 때문에 무시하고 넘어갈 수 있습니다. 하지만 네트워크 이슈로 예외가 발생한 경우는 다르게 봐야 합니다. 이 경우에도 skip을 해야 할까요? 물론 skip을 한다고 해서 배치 자체가 바로 실패하지는 않을 수 있습니다. 하지만 중요한 점은, 이 데이터는 잘못된 데이터가 아니라 정상 데이터라는 것입니다. 단지 외부 API 호출이나 네트워크 문제로 인해 일시적으로 처리하지 못했을 뿐입니다. 그런데 이를 skip해버리면 정상 데이터를 처리하지 않고 넘어간 것이 됩니다. 즉, 데이터 누락이 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;460&quot; data-start=&quot;311&quot; data-ke-size=&quot;size16&quot;&gt;따라서 네트워크 타임아웃이나 일시적인 5XX 오류처럼 복구 가능성이 있는 장애라면 skip보다는 retry를 먼저 고려하는 것이 안전합니다.&lt;br /&gt;retry 후에도 계속 실패한다면, 별도의 실패 이력으로 남기거나 배치를 실패시켜 후속 조치가 가능하도록 만들어야 합니다.&lt;/p&gt;
&lt;h2 data-end=&quot;460&quot; data-start=&quot;311&quot; data-ke-size=&quot;size26&quot;&gt;이번에는 skip이 아니라 retry를 적용해봅시다.&lt;/h2&gt;
&lt;p data-end=&quot;117&quot; data-start=&quot;33&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 retry 대상은 비즈니스 예외가 아니라 네트워크 이슈입니다. 그렇기 때문에 커스텀 Exception의 위치도 다르게 가져가는 것이 좋습니다. 예를 들어 skip 대상 Exception은 처리 단계에서 발생하는 데이터 검증 실패나 비즈니스 규칙 위반에 가깝기 때문에 process 하위 패키지에 둘 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;217&quot; data-ke-size=&quot;size16&quot;&gt;하지만 retry 대상 Exception은 성격이 다릅니다. 네트워크 타임아웃, 일시적인 5XX 응답, 외부 API 호출 실패처럼 외부 시스템과의 통신 과정에서 발생하는 예외에 가깝습니다. 따라서 이를 비즈니스 예외와 같은 위치에 두면 예외의 의미가 섞일 수 있습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;br /&gt;만약 같은 위치에서 관리하고 싶다면, exception 패키지를 별도로 만들고 그 안에서 skip과 retry에 사용되는 예외를 함께 관리하는 것이 더 적절하다고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIfK0C/dJMcacQxnZl/bNgdnWRuPactBXojkcVpz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIfK0C/dJMcacQxnZl/bNgdnWRuPactBXojkcVpz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIfK0C/dJMcacQxnZl/bNgdnWRuPactBXojkcVpz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIfK0C%2FdJMcacQxnZl%2FbNgdnWRuPactBXojkcVpz0%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;465&quot; height=&quot;451&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;217&quot; data-ke-size=&quot;size16&quot;&gt;retry Exception은 비즈니스 예외가 아니라 외부 통신 과정에서 발생하는 예외에 가깝습니다. 따라서 이를 process 내부의 비즈니스 코드에서 직접 사용하는 것은 적절하지 않습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;br /&gt;그래서 저는 Mapper 클래스를 만들어 적용하기로 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;187&quot; data-start=&quot;38&quot; data-ke-size=&quot;size16&quot;&gt;retry 대상 예외는 비즈니스 예외가 아니라 외부 통신 과정에서 발생하는 예외에 가깝습니다. 따라서 이를 process 내부의 비즈니스 코드에 직접 넣기보다는, 외부 API 응답이나 예외를 배치에서 사용할 수 있는 형태로 변환하는 계층이 필요하다고 판단했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv1ifJ/dJMcafzKGMq/ZF21Cjipk86EGK7lvcZddK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv1ifJ/dJMcafzKGMq/ZF21Cjipk86EGK7lvcZddK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv1ifJ/dJMcafzKGMq/ZF21Cjipk86EGK7lvcZddK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv1ifJ%2FdJMcafzKGMq%2FZF21Cjipk86EGK7lvcZddK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;644&quot; height=&quot;358&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;217&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 구성해두면 외부에서 어떤 예외가 발생했는지와 관계없이, 내부에서는 일관된 방식으로 처리할 수 있습니다.&lt;br /&gt;즉, 외부 라이브러리나 API의 예외에 직접 의존하는 것이 아니라, &lt;br /&gt;내부에서 정의한 예외 기준으로 retry나 실패 정책을 적용할 수 있게 됩니다. 또한 해당 코드는 상태를 가지는 객체가 아니라 단순히 예외를 변환하는 역할만 수행합니다. 즉, 특정 값을 저장하거나 관리하지 않고 입력값을 기반으로 결과만 반환하는 유틸성 메소드에 가깝습니다. 그래서 저는 이 코드를 static으로 구성했습니다. 굳이 Spring Bean으로 등록해서 관리할 필요가 없다고 판단했기 때문입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1862&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJVIPm/dJMcadhCpuU/dfKtFOJze5G4r3mctm9IY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJVIPm/dJMcadhCpuU/dfKtFOJze5G4r3mctm9IY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJVIPm/dJMcadhCpuU/dfKtFOJze5G4r3mctm9IY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJVIPm%2FdJMcadhCpuU%2FdfKtFOJze5G4r3mctm9IY1%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;734&quot; height=&quot;280&quot; data-origin-width=&quot;1862&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-end=&quot;367&quot; data-start=&quot;217&quot; data-ke-size=&quot;size26&quot;&gt;둘 다 해당되지 않는 경우는?&lt;/h2&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;217&quot; data-ke-size=&quot;size16&quot;&gt;여기서 한 가지 의문이 듭니다. skip도 적용했고, retry도 적용했습니다. 그렇다면 두 가지에 모두 해당하지 않는 예외는 어떻게 처리해야 할까요? 이 경우에는 무리하게 skip이나 retry 대상으로 포함시키면 안 됩니다.&lt;br /&gt;skip은 &quot;해당 데이터를 버려도 되는 경우&quot;에 사용하고, retry는 &quot;다시 시도하면 성공할 가능성이 있는 경우&quot;에 사용합니다.&lt;/p&gt;
&lt;p data-end=&quot;268&quot; data-start=&quot;206&quot; data-ke-size=&quot;size16&quot;&gt;반대로 두 조건에 모두 해당하지 않는 예외라면, 이는 배치가 계속 진행해도 안전한지 판단하기 어려운 예외입니다.&lt;/p&gt;
&lt;p data-end=&quot;364&quot; data-start=&quot;270&quot; data-ke-size=&quot;size16&quot;&gt;따라서 이런 예외는 기본적으로 배치를 실패시키는 것이 더 안전합니다.&lt;br /&gt;예상하지 못한 예외를 억지로 무시하면 데이터 누락이나 잘못된 처리로 이어질 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;366&quot; data-ke-size=&quot;size16&quot;&gt;즉 핵심은 단순합니다. 결국 배치에서는 &quot;처리해도 되는 실패&quot;와 &quot;멈춰야 하는 실패&quot;를 구분하는 것이 중요합니다.&lt;br /&gt;위 서두에서 말한 외부 API 스펙 변경이 바로 여기에 해당됩니다. 스펙 변경은 특정 row의 데이터 품질 문제가 아니기 때문에 skip 대상이 아닙니다. 또한 네트워크 타임아웃이나 5XX처럼 일시적인 장애도 아니기 때문에 retry 대상도 아닙니다.&lt;br /&gt;&lt;br /&gt;즉, 다시 시도한다고 해결되지 않고, 무시하고 넘어가도 안 되는 예외입니다. 이런 경우에는 배치를 계속 진행시키는 것보다 실패시키는 것이 더 안전합니다. &lt;br /&gt;왜냐하면 배치가 기대하던 응답 구조와 실제 응답 구조가 달라졌다는 것은, 이후 처리 결과를 신뢰하기 어렵다는 의미이기 때문입니다. 따라서 외부 API 스펙 변경처럼 skip과 retry 어디에도 해당하지 않는 예외는 fail 처리하여 빠르게 감지할 수 있도록 해야 합니다.&lt;/p&gt;
&lt;h2 data-end=&quot;367&quot; data-start=&quot;217&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;마무리&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서 학습한 핵심은 실패를 어떻게 관리할 것인가에 있습니다. 배치에서 발생하는 모든 예외를 동일하게 처리해서는 안 됩니다. 무조건 실패시키는 것이 정답도 아니고, 반대로 모든 예외를 skip하거나 retry하는 것도 올바른 방식은 아닙니다. 어떤 예외는 무시해도 되는 데이터 품질 문제일 수 있습니다. 반면 어떤 예외는 일시적인 네트워크 장애이기 때문에 재시도를 통해 복구할 수 있습니다. 그리고 외부 API 스펙 변경처럼 시스템의 계약 자체가 깨진 경우라면, 빠르게 실패를 감지하고 배치를 중단하는 것이 더 안전할 수 있습니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;결국 안정적인 프로젝트를 구성하기 위해서는 &amp;ldquo;성공 로직&amp;rdquo;만큼이나 &amp;ldquo;실패를 어떻게 관리할 것인가&amp;rdquo;에 대한 설계 역시 중요하다고 생각합니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/575</guid>
      <comments>https://b-programmer.tistory.com/575#entry575comment</comments>
      <pubDate>Wed, 6 May 2026 22:09:26 +0900</pubDate>
    </item>
    <item>
      <title>java에서 println()을 사용하면 안되는 이유</title>
      <link>https://b-programmer.tistory.com/574</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;몇 일 전, 긴 공백기를 지나 새로운 회사에 입사하게 되었습니다. 다행히도 저를 불러주는 곳이 있었고, 그 기회를 잡게 되었습니다. 해당 회사는 주로 SI 프로젝트를 수행하지만, 자체 솔루션도 함께 운영하고 있는 것으로 보입니다. 아직 구체적으로 어떤 프로젝트에 투입될지는 정해지지 않았습니다. 사전에 코드를 간단히 살펴보던 중, 로그를 println()으로 출력하는 방식이 사용되고 있는 것을 확인했습니다. 감각적으로는 println()을 로깅 용도로 사용하는 것이 적절하지 않다는 것을 알고 있었고, 실제로도 사용하지 않고 있었습니다. 다만, 왜 사용하면 안 되는지에 대해서는 명확하게 정리되어 있지 않았습니다. 그래서 출근 전에, println()을 로그로 사용하는 것이 왜 적절하지 않은지에 대해 스스로 정리하고 학습해보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;println을 어째서 사용하지 말라고 하는걸까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;println()은 출력 도구이지 로깅 도구가 아니기 때문입니다.&lt;br /&gt;그 차이는 운영환경에서 꽤 많이 들어납니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;System.out.println()은 내부적으로 동기화가 걸려 있습니다.&lt;br /&gt;즉, 여러 스레드가 동시에 호출하면 병목이 생깁니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjk8Ij/dJMcaf7vhA8/KffIUpFiuTgNxGNIZEKpD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjk8Ij/dJMcaf7vhA8/KffIUpFiuTgNxGNIZEKpD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjk8Ij/dJMcaf7vhA8/KffIUpFiuTgNxGNIZEKpD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjk8Ij%2FdJMcaf7vhA8%2FKffIUpFiuTgNxGNIZEKpD1%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;432&quot; height=&quot;303&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, synchronized를 걸게 되면 어째서 성능 문제가 발생할까요?&lt;br /&gt;동기화가 걸린다는 것은 스레드와 밀접하게 연결된 개념입니다. synchronized가 적용된다는 것은 여러 스레드가 동시에 접근하더라도, 한 번에 하나의 스레드만 해당 작업을 수행하도록 보장한다는 의미입니다. 이러한 구조는 데이터의 일관성을 맞추는 데에는 유리하지만, 동시에 여러 스레드를 직렬화시키기 때문에 성능 저하의 원인이 될 수 있습니다. 특히 요청이 많은 환경에서는 스레드들이 락을 획득하기 위해 대기하게 되고, 그로 인해 전체 처리량이 감소하는 병목 현상이 발생합니다.&lt;/p&gt;
&lt;p data-end=&quot;361&quot; data-start=&quot;281&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 Java는 왜 이러한 선택을 했을까요? 그 이유는 println()의 목적이 &lt;u&gt;성능이 아닌 정확한 출력&lt;/u&gt;에 있기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;541&quot; data-start=&quot;363&quot; data-ke-size=&quot;size16&quot;&gt;출력은 단순히 데이터를 내보내는 행위가 아니라, 메시지의 형태를 유지한 채 온전히 전달되는 것이 중요합니다. 만약 동기화가 없다면 여러 스레드가 동시에 출력하면서 문자열이 서로 뒤섞일 수 있습니다. 이 경우 출력 자체가 사라지는 것은 아니지만, 문장이 깨져 의미를 해석할 수 없는 상태가 될 수 있습니다. 따라서 Java는 출력의 일관성을 보장하기 위해 synchronized를 적용했고, &lt;br /&gt;그 결과 각 출력이 원자적으로 실행되도록 설계되었습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;449&quot; data-start=&quot;427&quot; data-section-id=&quot;ad2al&quot; data-ke-size=&quot;size23&quot;&gt;로그 레벨이 존재하지 않음&lt;/h3&gt;
&lt;p data-end=&quot;1680&quot; data-start=&quot;1629&quot; data-ke-size=&quot;size16&quot;&gt;로그 레벨이 존재하지 않는다는 점도 중요한 문제입니다.&lt;br /&gt;println()은 단순히 메시지를 출력하는 도구이기 때문에, 출력되는 정보에 대한 구분이 존재하지 않습니다.&lt;/p&gt;
&lt;p data-end=&quot;168&quot; data-start=&quot;102&quot; data-ke-size=&quot;size16&quot;&gt;즉, 어떤 로그가 단순한 정보인지, 디버깅을 위한 것인지, 혹은 실제 장애 상황을 나타내는 것인지 판단하기 어렵습니다.&lt;/p&gt;
&lt;p data-end=&quot;335&quot; data-start=&quot;170&quot; data-ke-size=&quot;size16&quot;&gt;반면, 로깅 프레임워크를 사용하면 debug, info, warn, error와 같이 로그의 성격에 따라 레벨을 나누어 관리할 수 있습니다.&lt;br /&gt;이를 통해 운영 환경에서는 특정 레벨 이상의 로그만 출력하도록 설정하는 등, 상황에 맞게 로깅을 제어하는 것이 가능합니다.&lt;/p&gt;
&lt;p data-end=&quot;454&quot; data-start=&quot;337&quot; data-ke-size=&quot;size16&quot;&gt;하지만 println()은 이러한 제어가 불가능합니다.&lt;br /&gt;출력 여부를 조정하려면 결국 코드 수정이 필요하며, 경우에 따라서는 조건문을 추가하는 방식으로 억지스럽게 제어해야 하는 상황이 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;575&quot; data-start=&quot;456&quot; data-ke-size=&quot;size16&quot;&gt;또한 로깅 프레임워크를 활용하면, 이러한 로그를 단순 콘솔 출력이 아닌 파일로 저장하고 관리하는 것도 가능해집니다.&lt;br /&gt;이를 통해 로그를 지속적으로 보관하거나, 장애 분석 및 모니터링에 활용할 수 있습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;575&quot; data-start=&quot;456&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼마나 빠르게 만들려고 했는지는 모르겠지만, 생각보다 코드가 엉망이었습니다. 패키지 구조는 상황에 따라 달라질수 있다고 여겨지지만 로깅만큼은 참을 수 없었습니다. 그래서 왜 println()을 사용하면 안되는지에 대해 학습을 진행하였습니다. 다음에는 로깅 프레임워크를 어떻게 사용하면 좋은지에 대해 학습해보겠습니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/이력관리</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/574</guid>
      <comments>https://b-programmer.tistory.com/574#entry574comment</comments>
      <pubDate>Fri, 1 May 2026 20:05:13 +0900</pubDate>
    </item>
    <item>
      <title>카프카의 설정은 진짜일까?? - offset(3)﻿</title>
      <link>https://b-programmer.tistory.com/573</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 학습한 내용을 간단하게 정리해보면, offset은 broker에 저장된 메시지의 순서를 의미하고, consumer는 이 offset을 기준으로 메시지를 읽고 처리하게 됩니다.&lt;br /&gt;&lt;br /&gt;그리고 어디까지 처리했는지를 기록하는 과정을 offset commit이라고 합니다. 이 commit 방식에는 크게 두 가지가 존재합니다. 자동 commit은 일정 주기에 따라 offset을 자동으로 기록해주기 때문에 구현이 간단하다는 장점이 있습니다. 하지만 처리 완료 여부와 관계없이 offset이 기록되기 때문에, 메시지를 처리하는 도중 애플리케이션에 장애가 발생할 경우 문제가 발생할 수 있습니다. &lt;br /&gt;&lt;br /&gt;이 경우 Kafka는 해당 메시지를 이미 처리된 것으로 인식하게 되고, consumer가 재시작되더라도 메시지는 다시 전달되지 않습니다. 결과적으로 데이터 유실이 발생할 수 있습니다.&lt;br /&gt;반면 수동 commit은 개발자가 직접 commit 시점을 제어해야 하기 때문에 다소 번거롭지만, &lt;br /&gt;처리 흐름을 보다 명확하게 관리할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자동 commit은 어떻게 하는지 대충 알겠는데 수동 commit은 도대체 어떻게 하는 걸까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 확인하기 전에 먼저 동작 흐름부터 이해해보겠습니다. &lt;br /&gt;수동 commit은 말 그대로 consumer가 메시지를 처리한 뒤, 개발자가 직접 commit 시점을 결정하는 방식입니다. &lt;br /&gt;consumer는 poll()을 통해 broker로부터 메시지를 가져옵니다. 그리고 가져온 메시지를 바탕으로 비즈니스 로직을 처리한 뒤, &lt;br /&gt;&lt;u&gt;처리가 완료되었다고 판단되는 시점에 commit을 수행합니다.&amp;nbsp;&lt;/u&gt; &lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;여기서 주목해야 할 점은 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;commit 시점을 개발자가 직접 결정할 수 있다는 것&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;입니다. &lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, 자동 commit처럼 일정 주기에 의해 offset이 기&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;록되는 것이 아니라, &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;u&gt;처리가 끝났다고 판단한 시점&lt;/u&gt;에 직접 offset을 기록할 수 있습니다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;수동 commit은 말 그대로 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;consumer가 메시지를 처리한 뒤, 개발자가 직접 commit 시점을 결정하는 방식&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;그렇다면, 어떻게 처리가 끝났다고 판단할 수 있을까?&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 1.62em;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;p data-end=&quot;162&quot; data-start=&quot;118&quot; data-ke-size=&quot;size16&quot;&gt;이를 이해하기 위해서는 Acknowledgment 개념을 알아야 합니다. &lt;br /&gt;Acknowledgment는 간단하게 말해, 메시지 처리가 완료되었음을 Kafka에 알리는 신호입니다.&lt;/p&gt;
&lt;p data-end=&quot;162&quot; data-start=&quot;118&quot; data-ke-size=&quot;size16&quot;&gt;consumer는 메시지를 처리한 뒤 Acknowledgment를 호출하게 되며, &lt;br /&gt;이 시점에 commit이 수행되거나 commit이 가능한 상태가 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btWabS/dJMcagL5vVa/j8biJFf7GRy5iQPP9xrNgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btWabS/dJMcagL5vVa/j8biJFf7GRy5iQPP9xrNgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btWabS/dJMcagL5vVa/j8biJFf7GRy5iQPP9xrNgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtWabS%2FdJMcagL5vVa%2Fj8biJFf7GRy5iQPP9xrNgK%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;749&quot; height=&quot;155&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;Acknowledgment는 commit 시점을 어떻게 제어하느냐에 따라 다양한 방식으로 동작하며, 내부적으로는 여러 구현체를 통해 처리됩니다. 대표적으로는 RECORD, BATCH, MANUAL 등의 방식이 있으며, 이러한 방식에 따라 commit 시점이 달라지게 됩니다.&lt;/p&gt;
&lt;div&gt;여기서 드는 의문은 어떻게 RECORD인지 BATCH인지 MANUAL인지 알고 다양한 구현체를 리턴해주는 걸까요?&lt;/div&gt;
&lt;div&gt;Acknowledgment의 동작 방식은 개발자가 직접 선택하는 것이 아니라, config나 yml에 설정된 AckMode 값을 기준으로 결정됩니다.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMWINb/dJMcahRIN5T/vAvfaWjGfWnfk8EmAkjV00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMWINb/dJMcahRIN5T/vAvfaWjGfWnfk8EmAkjV00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMWINb/dJMcahRIN5T/vAvfaWjGfWnfk8EmAkjV00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMWINb%2FdJMcahRIN5T%2FvAvfaWjGfWnfk8EmAkjV00%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;1024&quot; height=&quot;106&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;그렇다면 왜 이렇게 하는걸까요? 이유는 단순합니다. 메시지 처리 방식에 따라 commit 시점이 달라지기 때문입니다.&lt;br /&gt;어떤 경우에는 메시지를 하나씩 처리한 뒤 commit하는 것이 적절할 수 있고,&lt;br /&gt;어떤 경우에는 여러 메시지를 한 번에 처리한 뒤 commit하는 것이 더 효율적일 수 있습니다.&lt;/div&gt;
&lt;div&gt;조금더 직관적으로 말씀드리자면, RECORD는 하나씩 안전하게, BATCH는 묶어서 효율적으로 MANUAL은 내가 직접 제어 하는 방법입니다. 그나저나 이상합니다. 분명 이 챕터는 수동 commit에 관한 방식인데 RECORD, BATCH는 자동 commit인 걸까요?&lt;/div&gt;
&lt;div&gt;결론부터 말하면 그렇지 않습니다.&lt;/div&gt;
&lt;div&gt;RECORD와 BATCH는 Kafka의 자동 commit이 아니라, Spring Kafka의 컨테이너가 메시지 처리 이후 commit을 대신 수행해주는 방식입니다.&amp;nbsp;&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;그렇다면, MANAL방식은 어떻게 사용하는 걸까?&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1342&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mbBBe/dJMcahqGFnt/8jq5YoGlk0r6MHxQ71QbaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mbBBe/dJMcahqGFnt/8jq5YoGlk0r6MHxQ71QbaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mbBBe/dJMcahqGFnt/8jq5YoGlk0r6MHxQ71QbaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmbBBe%2FdJMcahqGFnt%2F8jq5YoGlk0r6MHxQ71QbaK%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;1342&quot; height=&quot;374&quot; data-origin-width=&quot;1342&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-turn-start-message=&quot;true&quot; data-message-model-slug=&quot;gpt-5-5-thinking&quot; data-message-id=&quot;df810530-500d-4557-88df-d7f2ff5d0266&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-end=&quot;24&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;사용하는 방식 자체는 크게 어렵지 않습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1777212632220&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;acknowledgment.acknowledge();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;164&quot; data-start=&quot;69&quot; data-ke-size=&quot;size16&quot;&gt;를 호출하면, consumer가 처리한 offset이 commit됩니다.&lt;br /&gt;즉, broker 입장에서는 이 메시지는 처리 완료되었다고 판단할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;189&quot; data-start=&quot;166&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 여기서 한 가지 의문이 생깁니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1777212648997&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;acknowledgment.acknowledge();
couponIssueRequestHandler.handle(event);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;353&quot; data-start=&quot;275&quot; data-ke-size=&quot;size16&quot;&gt;처럼 실제 처리 로직인 couponIssueRequestHandler.handle()을 실행하기 전에 commit을 하면 어떻게 될까요?&lt;/p&gt;
&lt;p data-end=&quot;375&quot; data-start=&quot;355&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;510&quot; data-start=&quot;377&quot; data-ke-size=&quot;size16&quot;&gt;commit이 먼저 수행되면 Kafka는 해당 메시지를 이미 처리된 것으로 인식합니다.&lt;br /&gt;그런데 그 이후 handle() 실행 중 애플리케이션 장애가 발생한다면, consumer가 재시작되더라도 해당 메시지는 다시 가져오지 않습니다.&lt;/p&gt;
&lt;p data-end=&quot;567&quot; data-start=&quot;512&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;결과적으로 처리되지 않은 메시지가 처리된 것으로 기록되어 데이터 유실이 발생할 수 있습니다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-end=&quot;608&quot; data-start=&quot;569&quot; data-ke-size=&quot;size16&quot;&gt;따라서 MANUAL commit에서는 보통 다음 순서가 되어야 합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1777212660197&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;couponIssueRequestHandler.handle(event);
acknowledgment.acknowledge();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;284&quot; data-start=&quot;222&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 단순합니다. commit은 처리 전에 하는 것이 아니라, 처리 완료 이후에 해야 합니다. 다만 commit을 먼저 수행하게 되면,&lt;br /&gt;메시지를 처리하기 전에 이미 offset이 기록되기 때문에 consumer 입장에서의 처리 지연(latency)은 줄어들 수 있습니다. &lt;br /&gt;하지만 이 경우에는 중요한 문제가 발생합니다. 처리 도중 장애가 발생하더라도 해당 메시지는 다시 전달되지 않습니다.&lt;br /&gt;&lt;br /&gt;즉, 데이터 유실 가능성을 감수해야 합니다. 따라서 이러한 방식은 정합성보다 처리 속도를 우선하는 경우에만 제한적으로 고려될 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;div&gt;벌써 2번째 완료를 미루고 있네요. 원래 이번글에 종료하고 싶었는데 점점길어지네요. 다음에 완료하는 것을 목표로 해보겠습니다.&lt;br /&gt;다음 시간에는 이번에 알아보지 못한 BATCH와 RECORD에 대해 학습해보겠습니다.&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;704&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;704&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-end=&quot;599&quot; data-start=&quot;583&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/573</guid>
      <comments>https://b-programmer.tistory.com/573#entry573comment</comments>
      <pubDate>Sun, 26 Apr 2026 23:16:32 +0900</pubDate>
    </item>
    <item>
      <title>카프카의 설정은 진짜일까?? - offset(2)﻿</title>
      <link>https://b-programmer.tistory.com/572</link>
      <description>&lt;figure id=&quot;og_1776848435105&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;카프카의 설정은 진짜일까?? - offset(1)&quot; data-og-description=&quot;원래 시나리오대로면, producer를 전부 끝내고 나서 consumer를 학습하려고 하였지만 살짝 우회하기로 결정하였습니다. 그 이유가 뭐냐면 카프카의 주요 특징중 하나가 offset으로 알고 있습니다. 제&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/566&quot; data-og-url=&quot;https://b-programmer.tistory.com/566&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ccCOKi/dJMb8QMekf4/HxVG43otBGo4KYmenYYkI0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/qmI7C/dJMb9iaTsEW/KqkqZUL91PS4JOllvGHkd0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/beeJj3/dJMb9hC3Bop/KgDOiNEEJX0AWUcorsWYuk/img.jpg?width=383&amp;amp;height=248&amp;amp;face=108_72_174_144&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/566&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/566&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ccCOKi/dJMb8QMekf4/HxVG43otBGo4KYmenYYkI0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/qmI7C/dJMb9iaTsEW/KqkqZUL91PS4JOllvGHkd0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/beeJj3/dJMb9hC3Bop/KgDOiNEEJX0AWUcorsWYuk/img.jpg?width=383&amp;amp;height=248&amp;amp;face=108_72_174_144');&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;카프카의 설정은 진짜일까?? - offset(1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;원래 시나리오대로면, producer를 전부 끝내고 나서 consumer를 학습하려고 하였지만 살짝 우회하기로 결정하였습니다. 그 이유가 뭐냐면 카프카의 주요 특징중 하나가 offset으로 알고 있습니다. 제&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 Kafka 관련 글을 작성하게 되었습니다. 이번 글에서는&lt;b&gt; offset commit&lt;/b&gt;에 대해 알아보려고 합니다. Kafka의 offset은 기본적으로 broker에 저장됩니다. 하지만 이 offset을 기준으로 어디까지 처리했는지 관리하는 주체는 consumer입니다. 그 이유는 broker와consumer의 역할이 다르기 때문입니다. broker는 메시지를 저장하고 전달하는 역할을 할 뿐, 우리 서비스의 비즈니스 로직을 직접 수행하지는 않습니다. 실제 메시지를 전달받아 처리하는 쪽은 consumer입니다. 따라서 consumer는 자신이 어디까지 처리했는지 직접 기록할 필요가 있는데, 이 과정을 offset commit이라고 부릅니다. 이번 글에서는 offset commit이 왜 필요한지, 그리고 어떤 방식으로 동작하는지 정리해보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;언제, 어떻게 commit이 발생하는지를 &amp;nbsp;직접 확인할 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 전에 Kafka의 전체 흐름부터 다시 짚어보겠습니다. Kafka는 기본적으로 producer &amp;rarr; broker &amp;rarr; consumer 구조로 동작합니다.&lt;br /&gt;producer는 메시지를 broker에 저장하고, consumer는 broker로부터 메시지를 가져와 처리합니다. &lt;br /&gt;이때 중요한 포인트가 하나 있습니다. broker에는 메시지가 계속 쌓일 수 있지만, consumer는 그 속도를 따라가지 못할 수도 있습니다.&lt;br /&gt;이렇게 아직 consumer가 처리하지 못한 메시지의 차이를 Kafka에서는 LAG이라고 부릅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LAG와 Commit의 관계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LAG는 다음과 같은 기준으로 계산됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1776860782596&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LAG = LOG-END-OFFSET - CURRENT-OFFSET&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 두 가지 값입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;503&quot; data-start=&quot;402&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;450&quot; data-start=&quot;402&quot; data-section-id=&quot;3vro3m&quot;&gt;&lt;b&gt;LOG-END-OFFSET&lt;/b&gt;&lt;br /&gt;&amp;rarr; broker에 쌓인 마지막 offset&lt;/li&gt;
&lt;li data-end=&quot;503&quot; data-start=&quot;452&quot; data-section-id=&quot;1dk4b47&quot;&gt;&lt;b&gt;CURRENT-OFFSET&lt;/b&gt;&lt;br /&gt;&amp;rarr; consumer가 commit한 offset&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;530&quot; data-start=&quot;505&quot; data-ke-size=&quot;size16&quot;&gt;이 의미를 조금 더 풀어보면 다음과 같습니다. producer는 계속해서 메시지를 broker에 적재하고,&lt;br /&gt;consumer는 메시지를 처리한 뒤 offset을 commit합니다. &lt;br /&gt;이때 &lt;u&gt;consumer의 처리 속도가 producer의 적재 속도를 따라가지 못하거나, commit이 지연되면 두 offset의 차이가 벌어지게 됩니다.&lt;/u&gt; &amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;530&quot; data-start=&quot;505&quot; data-ke-size=&quot;size16&quot;&gt;이 차이가 바로 LAG입니다. 반대로 consumer가 메시지를 처리하고 commit을 진행하면 CURRENT-OFFSET이 증가하면서&lt;br /&gt;LAG는 점점 줄어들게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;530&quot; data-start=&quot;505&quot; data-ke-size=&quot;size16&quot;&gt;결국 LAG가 증가한다는 것은 broker에서 consumer로 메시지가 덜 전달되는 것이 아니라,&lt;br /&gt;consumer가 메시지를 충분히 빠르게 처리하지 못해 처리되지 않은 메시지가 누적되고 있는 상태를 의미합니다.&lt;/p&gt;
&lt;h2 data-end=&quot;530&quot; data-start=&quot;505&quot; data-ke-size=&quot;size26&quot;&gt;그렇다면 broker로부터 전달받은 메시지는 consumer에서 어떻게 처리될까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer는 메시지를 단순히 읽는 것이 아니라, 해당 메시지에 대해 비즈니스 로직을 수행하게 됩니다.&lt;br /&gt;하지만 여기서 한 가지 중요한 문제가 있습니다. &lt;br /&gt;Kafka는 메시지를 전달해줄 뿐, 해당 메시지가 실제로 처리되었는지는 알 수 없습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;따라서 consumer는 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 메시지까지는 처리 완료했다는 상태를 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;직접 기록해야 합니다.&lt;br /&gt;&lt;/span&gt;이때 사용하는 개념이 바로 commit입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;commit은 수동 commit과 자동 commit이 존재합니다. 그렇다면, 수동과 자동으로 왜 분리가 되었을까요?&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;자동 commit&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 단순합니다.&lt;br /&gt;언제 처리 완료로 볼 것인가를 누가 결정하느냐의 차이입니다.&lt;/p&gt;
&lt;p data-end=&quot;373&quot; data-start=&quot;265&quot; data-ke-size=&quot;size16&quot;&gt;자동 commit은 일정 주기마다 offset을 자동으로 commit하는 방식입니다.&lt;br /&gt;consumer는 메시지를 poll로 가져온 이후, 설정된 주기에 따라 offset을 commit합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1776866986357&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;메시지 조회(poll) &amp;rarr; 일정 시간 경과 &amp;rarr; 자동 commit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 명확합니다. 구현이 단순하고 별도의 처리 없이도 offset이 관리됩니다.&lt;br /&gt;하지만 치명적인 문제가 존재합니다. 아직 처리가 끝나지 않았는데 commit이 발생할 수 있습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 다음과 같은 상황을 생각해볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1776867000173&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;poll &amp;rarr; 메시지 수신
(비즈니스 로직 처리 중)
&amp;rarr; 자동 commit 발생
&amp;rarr; 애플리케이션 장애 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 Kafka 입장에서는 이미 해당 메시지가 처리된 것으로 인식하게 됩니다. &lt;br /&gt;따라서 consumer가 재시작되더라도 해당 메시지는 다시 전달되지 않습니다. 결과적으로 데이터 유실 가능성이 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 말하는 Kafka는 무엇일까요? &lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;257&quot; data-start=&quot;194&quot; data-ke-size=&quot;size16&quot;&gt;기존에 우리가 말하는 Kafka는 broker였습니다. 그렇다면 여기서 말하는 Kafka도 broker일까요?&lt;br /&gt;정확히 말씀드리자면, 여기서 말하는 Kafka는 broker가 아닙니다.&lt;br /&gt;consumer client를 의미합니다.&lt;br /&gt;consumer client는 broker와 분리된 애플리케이션이지만, Kafka 프로토콜을 기반으로 통신하며 동작합니다.&lt;/p&gt;
&lt;p data-end=&quot;490&quot; data-start=&quot;406&quot; data-ke-size=&quot;size16&quot;&gt;즉, commit은 broker 내부에서 자동으로 처리되는 것이 아니라, consumer client가 주도적으로 수행하는 동작입니다.&lt;/p&gt;
&lt;p data-end=&quot;514&quot; data-start=&quot;492&quot; data-ke-size=&quot;size16&quot;&gt;이 점이 중요한 이유는 다음과 같습니다.&lt;/p&gt;
&lt;p data-end=&quot;593&quot; data-start=&quot;516&quot; data-ke-size=&quot;size16&quot;&gt;consumer client는 Kafka broker와 분리되어 있지만, 네트워크를 통해 연결된 상태에서 동작하기 때문에 실행 환경의 영향을 직접적으로 받습니다. 따라서 commit 시점은 항상 안정적으로 보장되는 것이 아니라, 애플리케이션의 상태에 따라 달라질 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;수동 commit&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수동 commit은 개발자가 직접 commit 시점을 결정하는 방식입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1776867568215&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;poll &amp;rarr; 메시지 받음 &amp;rarr; 처리 완료 &amp;rarr; commit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 가장 큰 특징은 처리 완료 이후에만 commit을 수행할 수 있다는 점입니다.&lt;br /&gt;따라서 메시지를 처리하기 전에 commit이 발생하는 상황을 방지할 수 있으며, 데이터 유실 가능성을 크게 줄일 수 있습니다.&lt;br /&gt;하지만 그만큼 고려해야 할 점도 존재합니다. 처리가 완료된 이후에 commit을 수행하기 때문에,&lt;br /&gt;commit 이전에 장애가 발생할 경우 동일한 메시지가 다시 전달될 수 있습니다.&lt;br /&gt;즉, 중복 처리 가능성이 발생할 수 있습니다. 또한 commit 시점을 직접 제어해야 하기 때문에, 구현 복잡도가 증가하는 단점도 존재합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-end=&quot;235&quot; data-start=&quot;167&quot; data-ke-size=&quot;size16&quot;&gt;오늘은 여기까지 정리해보겠습니다. 이번 글에서는 자동 commit과 수동 commit의 개념을 간단하게 살펴보았습니다. 그렇다면 수동 commit은 개발자가 직접 commit 시점을 제어한다고 했는데, 실제로 어떤 방식으로 처리되는지는 다음 글에서 자세히 다뤄보겠습니다.&lt;br /&gt;또한 consumer가 메시지를 처리할 때 정합성을 우선할 것인지, 처리 속도를 우선할 것인지 이와 관련된 다양한 방법들도 함께 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/572</guid>
      <comments>https://b-programmer.tistory.com/572#entry572comment</comments>
      <pubDate>Wed, 22 Apr 2026 23:47:49 +0900</pubDate>
    </item>
    <item>
      <title>웹소켓 vs mq</title>
      <link>https://b-programmer.tistory.com/571</link>
      <description>&lt;p data-end=&quot;580&quot; data-start=&quot;448&quot; data-ke-size=&quot;size16&quot;&gt;웹소켓과 MQ는 모두 데이터를 전달하는 통로라는 공통점을 가지고 있습니다. 겉보기에는 둘 다 어떤 큐를 통해 메시지를 주고받는 것처럼 느껴질 수 있습니다. 하지만 실제로는 사용하는 목적도 다르고, 시스템 안에서 맡는 역할도 다릅니다. 보통 웹소켓은 실시간성, MQ는 비동기 처리라고 설명합니다. 물론 틀린 말은 아닙니다. 다만 이렇게 이분법적으로 나누면 두 기술의 차이를 충분히 설명하기에는 다소 부족하다고 느껴졌습니다. 웹소켓도 결국 비동기적으로 동작하고, MQ 역시 상황에 따라 실시간에 가깝게 활용될 수 있기 때문입니다. 따라서 이 글에서는 단순한 특징 비교에 그치지 않고, 웹소켓과 MQ에서 말하는 &quot;큐&quot;를 각각 어떻게 이해해야 하는지에 초점을 맞춰 정리해보겠습니다.&lt;/p&gt;
&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;p data-ke-size=&quot;size16&quot;&gt;이것을 이해하기 위해서는 웹소켓이 왜 존재하는지부터 생각해야 합니다. 웹소켓은 기존 &lt;span&gt;&lt;span&gt;HTTP&lt;/span&gt;&lt;/span&gt; 위에서 동작하도록 만들어진 프로토콜입니다. 그렇다면, 왜 기존 방식을 유지하지 않고 새로운 방식을 만들었을까요?&lt;br /&gt;&lt;br /&gt;HTTP는 요청을 전달하고 응답을 반환하면 연결이 종료되는 구조입니다. 이러한 구조에서는 실시간 데이터가 필요한 서비스에서 문제가 발생했습니다. 예를 들어, 채팅이나 실시간 알림과 같은 서비스에서는 데이터의 변화가 발생할 때마다 클라이언트가 지속적으로 요청을 보내야 했습니다. 이 과정에서 불필요한 요청이 반복적으로 발생했고, 요청과 응답 사이클로 인해 지연이 발생했습니다.&lt;/p&gt;
&lt;p data-end=&quot;455&quot; data-start=&quot;434&quot; data-ke-size=&quot;size16&quot;&gt;결국 다음과 같은 문제가 발생했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;557&quot; data-start=&quot;457&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;495&quot; data-start=&quot;457&quot; data-section-id=&quot;17g10da&quot;&gt;지속적인 요청으로 인한 불필요한 네트워크 비용이 발생했습니다.&lt;/li&gt;
&lt;li data-end=&quot;527&quot; data-start=&quot;496&quot; data-section-id=&quot;18a9asq&quot;&gt;요청-응답 구조로 인해 지연 시간이 증가했습니다.&lt;/li&gt;
&lt;li data-end=&quot;557&quot; data-start=&quot;528&quot; data-section-id=&quot;ajfp5y&quot;&gt;서버와 클라이언트 모두에 부담이 증가했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;621&quot; data-start=&quot;559&quot; data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 &quot;요청을 반복하지 않고 데이터를 지속적으로 받을 수 있는 방법&quot;이 필요했습니다.&lt;br /&gt;그 결과로 등장한 것이 웹소켓입니다. 웹소켓은 한 번 연결을 맺으면 이를 유지한 채, 서버와 클라이언트가 자유롭게 데이터를 주고받을 수 있도록 설계되었습니다. 즉, 요청과 응답이 반복되는 구조에서 벗어나 데이터가 지속적으로 흐를 수 있도록 만들기 위해 등장한 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹소켓은 데이터가 지속적으로 흐를 수 있도록 하기 위해 등장한 기술입니다. 즉, MQ에서 말하는 큐의 개념과는 다릅니다.&lt;br /&gt;큐는 말 그대로 대기열입니다. 어떤 작업이나 메시지를 잠시 쌓아두고, 이후에 처리하기 위해 존재합니다. &lt;br /&gt;하지만 웹소켓에서의 통로는 이러한 대기열의 개념이 아닙니다. 데이터를 쌓아두기 위한 공간이 아니라, 흐르게 하기 위한 경로에 가깝습니다. 비유를 하자면 고속도로의 하이패스와 유사합니다. 일반적인 HTTP 요청은 톨게이트에서 매번 멈췄다가 지나가는 구조라면,&lt;br /&gt;웹소켓은 하이패스를 통해 멈추지 않고 계속 흐르는 구조라고 볼 수 있습니다.&lt;br /&gt;&lt;br /&gt;즉, 웹소켓은 데이터를 &quot;쌓기 위한 구조&quot;가 아니라 지속적으로 전달하기 위한 통로라고 이해하는 것이 더 적절합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2056&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUQafa/dJMcaarug4K/6cRN8ho8qArmb12aliazd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUQafa/dJMcaarug4K/6cRN8ho8qArmb12aliazd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUQafa/dJMcaarug4K/6cRN8ho8qArmb12aliazd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUQafa%2FdJMcaarug4K%2F6cRN8ho8qArmb12aliazd1%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;2056&quot; height=&quot;422&quot; data-origin-width=&quot;2056&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면 MQ는 어떨까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MQ는 이름 그대로 메시지를 큐에 적재하여 전달하는 것이 핵심입니다.&lt;br /&gt;간단히 설명하면 MQ는 producer와 consumer로 구성되며, producer에서 발생한 메시지를 consumer가 가져가서 처리하는 구조입니다.&lt;/p&gt;
&lt;p data-end=&quot;370&quot; data-start=&quot;308&quot; data-ke-size=&quot;size16&quot;&gt;즉, 메시지를 즉시 처리하는 것이 아니라, 큐에 적재해두고 나중에 처리하는 시스템이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;471&quot; data-start=&quot;372&quot; data-ke-size=&quot;size16&quot;&gt;이러한 구조를 사용하는 이유는 요청과 처리를 분리하기 위함입니다.&lt;br /&gt;요청을 받은 시점에서 모든 로직을 처리하지 않고, 메시지만 큐에 넣은 뒤 빠르게 응답을 반환할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;496&quot; data-start=&quot;473&quot; data-ke-size=&quot;size16&quot;&gt;그 결과로 다음과 같은 이점이 발생합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;579&quot; data-start=&quot;498&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;520&quot; data-start=&quot;498&quot; data-section-id=&quot;bx33wz&quot;&gt;요청 처리 시간이 단축되었습니다.&lt;/li&gt;
&lt;li data-end=&quot;543&quot; data-start=&quot;521&quot; data-section-id=&quot;1maf6v8&quot;&gt;시스템 간 결합도가 낮아졌습니다.&lt;/li&gt;
&lt;li data-end=&quot;579&quot; data-start=&quot;544&quot; data-section-id=&quot;jecev3&quot;&gt;트래픽이 몰리더라도 유연하게 대응할 수 있게 되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;673&quot; data-start=&quot;581&quot; data-ke-size=&quot;size16&quot;&gt;또한 MQ는 메시지를 순서대로 저장할 수 있는 구조를 가지고 있습니다. 하지만 중요한 점은, 빠른 전송의 핵심이 순서 자체에 있는 것은 아니라는 점입니다. 실제로는 메시지를 큐에 적재한 이후, consumer가 이를 비동기적으로 처리하거나 병렬로 처리할 수 있기 때문에 전체적인 처리 효율이 증가하게 됩니다. 즉, MQ는 단순히 메시지를 전달하는 것이 아니라 처리 시점을 뒤로 미루고, 시스템을 분리하기 위한 구조라고 이해하는 것이 더 적절합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2090&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beZsFE/dJMcaju9SB2/L11A7z0WxVS2gNkNwjKqA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beZsFE/dJMcaju9SB2/L11A7z0WxVS2gNkNwjKqA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beZsFE/dJMcaju9SB2/L11A7z0WxVS2gNkNwjKqA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeZsFE%2FdJMcaju9SB2%2FL11A7z0WxVS2gNkNwjKqA1%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;2090&quot; height=&quot;424&quot; data-origin-width=&quot;2090&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누가 보아도 서로 다른 두 기술을 &quot;통로&quot;라는 공통점으로 묶어 비교해보았습니다. 비교를 진행해보니, 언제 웹소켓을 사용하고 MQ를 사용해야 하는지 조금 더 명확하게 보였다고 느꼈습니다. 경우에 따라서는 웹소켓과 MQ를 함께 사용하는 구조도 존재합니다. 이렇게 두 기술을 함께 사용하는 이유는, 웹소켓이 제공하는 통로에서 동시성이 높아질 수 있기 때문입니다. 이로 인해 동일한 자원에 대해 동시에 접근하는 상황이 발생할 수 있으며, 이는 race condition으로 이어질 수 있습니다. 결과적으로 데이터가 의도하지 않은 형태로 처리될 가능성도 존재합니다. 이를 해결하기 위한 방법은 여러 가지가 있지만, 그 중 하나로 MQ를 활용하여 메시지를 큐에 적재하고 순차적으로 처리하는 방식도 고려할 수 있습니다. 앞으로도 이처럼 서로 다른 기술을 비교하고, 그 내부 동작을 깊이 있게 이해한다면 보다 나은 설계를 할 수 있을 것이라 생각합니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/571</guid>
      <comments>https://b-programmer.tistory.com/571#entry571comment</comments>
      <pubDate>Mon, 20 Apr 2026 22:51:20 +0900</pubDate>
    </item>
    <item>
      <title>race condition</title>
      <link>https://b-programmer.tistory.com/569</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Race condition은 여러 작업이 동시에 같은 데이터를 처리할 때, 실행 순서에 따라 결과가 달라지는 문제입니다.&lt;br /&gt;쉽게 말해, &lt;b&gt;같은 자원에 대한 접근이 동시에 발생하면서 기대한 결과와 실제 결과가 달라지는 상황&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;633&quot; data-start=&quot;596&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;1280&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DTlQ2/dJMcagLVDjC/ECEnK37XULHXTExHpKtHe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DTlQ2/dJMcagLVDjC/ECEnK37XULHXTExHpKtHe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DTlQ2/dJMcagLVDjC/ECEnK37XULHXTExHpKtHe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDTlQ2%2FdJMcagLVDjC%2FECEnK37XULHXTExHpKtHe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;313&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;313&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;510&quot; data-start=&quot;492&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 100명의 사용자가 동시에 쿠폰 발급을 요청했다고 가정하겠습니다. 응답 결과만 보면 6건만 성공했고, 94건은 실패했습니다.&lt;br /&gt;그렇다면 정상적인 시스템이라면 실제 DB에도 성공한 6건만 저장되어 있어야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;868&quot; data-start=&quot;763&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제로 확인해보니 DB에는 96건의 쿠폰 발급 내역이 저장되어 있었습니다.&lt;br /&gt;즉, 애플리케이션이 사용자에게 반환한 결과와 실제 데이터베이스 상태가 서로 일치하지 않았습니다.&lt;/p&gt;
&lt;p data-end=&quot;951&quot; data-start=&quot;870&quot; data-ke-size=&quot;size16&quot;&gt;이런 상황을 정합성이 깨졌다고 할 수 있습니다. 더 정확하게는, 동시성 문제로 인해 데이터 정합성이 깨진 상태라고 표현할 수 있습니다.&lt;br /&gt;반대로, 응답 결과가 6건 성공, 94건 실패이고 실제 DB에도 정확히 6건만 저장되었다면, 비록 실패 요청이 많았더라도 정합성은 유지된 것입니다. 즉, 정합성은 단순히 성공이나 실패 수가 많은지가 아니라, 시스템이 기대한 결과와 실제 저장된 데이터가 일치하는지로 판단해야 합니다. &lt;br /&gt;&lt;br /&gt;이처럼 race condition은 단순히 일부 요청이 실패하는 문제를 넘어, 성공/실패 응답과 실제 데이터 상태를 어긋나게 만들어 시스템 신뢰성을 무너뜨릴 수 있습니다. 그렇다면 이제 중요한 질문은 하나입니다. 이 race condition을 어떻게 제어하고, 데이터 정합성을 어떻게 보장할 것인가입니다.&lt;/p&gt;
&lt;h2 data-end=&quot;510&quot; data-start=&quot;492&quot; data-ke-size=&quot;size26&quot;&gt;race condtion이 왜 발생하는지 생각해봅시다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 동시성 문제로 인해 race condition이 발생한다고 설명했습니다.&lt;br /&gt;그렇다면 race condition이란 무엇일까요?&lt;/p&gt;
&lt;p data-end=&quot;389&quot; data-start=&quot;287&quot; data-ke-size=&quot;size16&quot;&gt;다음 그림을 보면 여러 요청이 동시에 동일한 자원에 접근하고 수정하는 상황을 확인할 수 있습니다.&lt;br /&gt;이때 각 요청은 독립적으로 처리되기 때문에 실행 순서가 보장되지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u6GFW/dJMcaakCwAL/BIFbU958zPZDFU1kiJD590/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u6GFW/dJMcaakCwAL/BIFbU958zPZDFU1kiJD590/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u6GFW/dJMcaakCwAL/BIFbU958zPZDFU1kiJD590/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu6GFW%2FdJMcaakCwAL%2FBIFbU958zPZDFU1kiJD590%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;618&quot; height=&quot;302&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 다음과 같은 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;456&quot; data-start=&quot;420&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;427&quot; data-start=&quot;420&quot; data-section-id=&quot;1rfqyhl&quot;&gt;중복 발급&lt;/li&gt;
&lt;li data-end=&quot;437&quot; data-start=&quot;428&quot; data-section-id=&quot;1yq9gy8&quot;&gt;데이터 불일치&lt;/li&gt;
&lt;li data-end=&quot;456&quot; data-start=&quot;438&quot; data-section-id=&quot;12jx4fr&quot;&gt;응답 결과와 DB 상태 불일치&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;543&quot; data-start=&quot;458&quot; data-ke-size=&quot;size16&quot;&gt;즉, 여러 요청이 서로의 상태를 고려하지 않은 채 동시에 실행되면서 예상하지 못한 결과가 발생하는 것이 바로 race condition입니다.&lt;br /&gt;race condition이라는 용어는 말 그대로 경주(race)에서 유래했습니다.&lt;br /&gt;&lt;br /&gt;여러 요청이 동시에 자원에 접근하면서 누가 먼저 실행될지 알 수 없는 상태,&lt;br /&gt;즉 제어되지 않은 경쟁 상황을 의미합니다.&lt;/p&gt;
&lt;p data-end=&quot;451&quot; data-start=&quot;432&quot; data-ke-size=&quot;size16&quot;&gt;여기서 알 수 있는 정보는 하나입니다. &lt;u&gt;실행 순서가 보장이 되지 않는 점&lt;/u&gt;입니다. &lt;br /&gt;그렇다면, 순서를 보장하기 위해서는 어떻게 할 수 있을까요?&lt;/p&gt;
&lt;h2 data-end=&quot;451&quot; data-start=&quot;432&quot; data-ke-size=&quot;size26&quot;&gt;순서를 보장하기 위해서는 어떻게 할 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 단순합니다. 여러 요청이 동시에 같은 자원을 수정하지 못하도록 만들어야 합니다.&lt;br /&gt;동시에 같은 자원을 수정하지 못하게 만들려면 어떻게 해야 할까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lock을 건다&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7Yw2K/dJMcaaEWZ1J/6FVTjEbqJYUJFS3qEY7NH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7Yw2K/dJMcaaEWZ1J/6FVTjEbqJYUJFS3qEY7NH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7Yw2K/dJMcaaEWZ1J/6FVTjEbqJYUJFS3qEY7NH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7Yw2K%2FdJMcaaEWZ1J%2F6FVTjEbqJYUJFS3qEY7NH1%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;543&quot; height=&quot;294&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1080&quot; data-start=&quot;1058&quot; data-ke-size=&quot;size16&quot;&gt;락을 사용하게 되면 동시에 하나의 요청만 자원에 접근할 수 있도록 제어할 수 있습니다.&lt;br /&gt;즉, 하나의 요청이 작업을 수행하는 동안 다른 요청들은 대기하게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;493&quot; data-start=&quot;359&quot; data-ke-size=&quot;size16&quot;&gt;이러한 방식은 race condition을 방지하고 데이터 정합성을 보장하는 데 효과적입니다.&lt;br /&gt;하지만 동시에 처리할 수 있는 요청 수가 제한되기 때문에,&lt;br /&gt;대기 시간이 발생하며 전체적인 응답 지연(latency)이 증가할 수 있습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;대표적인 방법은, synchronized, DB Lock, 분산 락등이 있습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;493&quot; data-start=&quot;359&quot; 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;1852&quot; data-origin-height=&quot;854&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYUOOk/dJMcahRzljQ/3uVQcd7K9dBwCwMJiXfNV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYUOOk/dJMcahRzljQ/3uVQcd7K9dBwCwMJiXfNV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYUOOk/dJMcahRzljQ/3uVQcd7K9dBwCwMJiXfNV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYUOOk%2FdJMcahRzljQ%2F3uVQcd7K9dBwCwMJiXfNV1%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;626&quot; height=&quot;289&quot; data-origin-width=&quot;1852&quot; data-origin-height=&quot;854&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1080&quot; data-start=&quot;1058&quot; data-ke-size=&quot;size16&quot;&gt;범위를 줄이는 방법은 lock을 사용하는 방식에 비해 근본적인 해결책은 아닙니다. 범위를 줄인다는 것은 동일한 자원에 대한 접근 빈도를 낮추는 것이지, 동시에 접근이 발생하는 상황 자체를 제거하는 것은 아니기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;630&quot; data-start=&quot;515&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 처리 대상이 100만 건에서 1000건으로 줄어들었다고 하더라도, 여전히 여러 요청이 동시에 같은 자원에 접근할 수 있으며,&lt;br /&gt;race condition이 발생하지 않는다는 보장은 없습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;다만 이 방식은 추가적인 락이나 큐를 사용하지 않기 때문에, 자원 사용 측면에서는 가장 부담이 적은 접근입니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;따라서 &lt;u&gt;race condition이 치명적이지 않거나, 충돌 가능성을 낮추는 것&lt;/u&gt;만으로도 충분한 경우라면&lt;br /&gt;&lt;/span&gt;범위를 줄이는 방식부터 적용하는 것이 효과적인 선택이 될 수 있습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1080&quot; data-start=&quot;1058&quot; 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;1852&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dM7iu2/dJMcahRzlSc/vSjDLb7v4YxcYJmpVkReSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dM7iu2/dJMcahRzlSc/vSjDLb7v4YxcYJmpVkReSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dM7iu2/dJMcahRzlSc/vSjDLb7v4YxcYJmpVkReSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdM7iu2%2FdJMcahRzlSc%2FvSjDLb7v4YxcYJmpVkReSk%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;696&quot; height=&quot;332&quot; data-origin-width=&quot;1852&quot; data-origin-height=&quot;884&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;510&quot; data-start=&quot;492&quot; data-ke-size=&quot;size16&quot;&gt;락을 사용하는 방법이나 범위를 줄이는 방법과 비교하면, 큐를 사용하는 방식도 비슷한 해결책처럼 보일 수 있습니다. &lt;br /&gt;이 점은 부정하지 않겠습니다.&lt;/p&gt;
&lt;p data-end=&quot;405&quot; data-start=&quot;344&quot; data-ke-size=&quot;size16&quot;&gt;하지만 큐를 사용하는 방식의 본질은 다릅니다. 요청과 실제 처리를 분리하는 구조라는 점입니다.&lt;/p&gt;
&lt;p data-end=&quot;542&quot; data-start=&quot;407&quot; data-ke-size=&quot;size16&quot;&gt;요청을 받는 시점에는 단순히 큐에 적재만 하고, 실제 처리는 이후에 별도의 consumer가 수행하게 됩니다.&lt;br /&gt;즉, 요청 처리 경로에서 무거운 작업을 제거할 수 있고, 그 결과 사용자 입장에서는 더 빠르게 응답을 받을 수 있습니다.&lt;br /&gt;또한 큐를 사용하면 다음과 같은 장점을 얻을 수 있습니다. &lt;br /&gt;&lt;br /&gt;첫 번째는 &lt;b&gt;처리 흐름을 제어할 수 있다는 점&lt;/b&gt;입니다. 단일 consumer를 사용한다면 요청을 순차적으로 처리할 수 있고,&lt;br /&gt;동시에 접근하면서 발생하는 race condition을 줄일 수 있습니다. &lt;br /&gt;&lt;br /&gt;두 번째는 &lt;b&gt;확장성&lt;/b&gt;입니다. 큐는 여러 개의 consumer를 통해 병렬 처리가 가능하기 때문에, 트래픽이 증가하더라도 유연하게 대응할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;786&quot; data-start=&quot;697&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 구조에도 중요한 함정이 존재합니다. &lt;br /&gt;첫 번째는 &lt;b&gt;큐에 들어가기 전 단계에서 race condition이 발생할 수 있다는 점&lt;/b&gt;입니다.&lt;br /&gt;즉, 큐에 넣기 전에 이미 공유 자원을 조회하거나 검증하는 로직이 있다면, 여전히 동시성 문제가 발생할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;두 번째는 &lt;/span&gt;&lt;b&gt;병렬 처리로 인한 race condition&lt;/b&gt;입니다. 여러 consumer를 사용해 병렬로 처리할 경우, 다시 동시에 같은 자원을 수정하는 상황이 발생할 수 있습니다. 이 경우 latency는 개선될 수 있지만, 정합성 문제가 다시 나타날 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;1130&quot; data-start=&quot;974&quot; data-ke-size=&quot;size16&quot;&gt;따라서 큐 기반 구조는 단순히 도입하는 것만으로는 충분하지 않고, 큐에 들어가기 전 단계와 consumer 처리 단계 모두에서&lt;br /&gt;동시성 제어를 함께 고려해야 합니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 두 가지 문제를 함께 해결할 수 있다면, &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;락 기반 방식이나 단순 범위 축소 방식보다 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;더 높은 성능과 정합성을 동시에 확보할 수 있는 구조라고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;813&quot; data-start=&quot;731&quot; data-ke-size=&quot;size16&quot;&gt;앞에서 설명한 여러 방법을 적용하더라도, 모든 상황에서 race condition을 완벽하게 제거할 수 있다고 보기는 어렵습니다. 중요한 것은 race condition을 무조건 없애는 것 자체보다, 어떤 구간에서 동시성 문제가 발생할 수 있는지를 인지하고, 그에 맞는 방법으로 제어하는 것이라고 생각합니다. 즉, 핵심은 race condition의 가능성을 이해하고, 문제가 발생할 수 있는 지점을 찾은 뒤, 락, 큐, 조건부 갱신, 범위 분산과 같은 적절한 방법을 상황에 맞게 적용하는 것입니다. 결국 실무에서 중요한 것은 동시성 문제를 완벽하게 통제할 수 있다고 믿는 것이 아니라, 문제가 발생할 수 있음을 전제로 설계하고 대응하는 것이라고 생각합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;813&quot; data-start=&quot;731&quot; data-ke-size=&quot;size16&quot;&gt;선착순 쿠폰 시스템을 개발하면서 2가지 문제가 발견이 되었습니다. 그 중 하나를 다음 시간에 적용해볼려고 합니다.&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;1102&quot; data-origin-height=&quot;1108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKPhU3/dJMcaaLHwj6/G0KjAvnPNh0X07B4C0Jzl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKPhU3/dJMcaaLHwj6/G0KjAvnPNh0X07B4C0Jzl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKPhU3/dJMcaaLHwj6/G0KjAvnPNh0X07B4C0Jzl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKPhU3%2FdJMcaaLHwj6%2FG0KjAvnPNh0X07B4C0Jzl1%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;470&quot; height=&quot;473&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;1108&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10000장의 쿠폰을 대상으로 500명의 동시 요청을 발생시키는 테스트를 진행했습니다. 그 결과, 일부 요청에서 총 63건의 실패가 발생했습니다. 이 시점에서는 큐를 통해 처리하기 이전 단계였기 때문에, HTTP 요청을 처리하는 과정에서 동시 접근이 발생했고, &lt;br /&gt;그로 인해 race condition이 발생했을 가능성이 높다고 판단했습니다. 이를 해결하기 위해, 요청 처리 구조를 다시 점검해보았습니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/569</guid>
      <comments>https://b-programmer.tistory.com/569#entry569comment</comments>
      <pubDate>Mon, 13 Apr 2026 21:47:26 +0900</pubDate>
    </item>
    <item>
      <title>batch - step,job</title>
      <link>https://b-programmer.tistory.com/568</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;배치의 전반적인 처리 과정에 대해 학습을 진행하였다. 배치는 기본적으로 3가지 단계로 동작한다.&lt;/p&gt;
&lt;pre id=&quot;code_1775827237397&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Reader &amp;rarr; Processor &amp;rarr; Writer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;147&quot; data-start=&quot;122&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 읽고, 가공하고, 저장하는 구조이다.&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;하지만 현재까지는 이 흐름을 단순하게 확인하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;배치를 강제로 실행시키는 방식으로 동작을 확인하고 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJz3gC/dJMcag6bTVW/N1jBm0iM7acOD2bT7iBn70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJz3gC/dJMcag6bTVW/N1jBm0iM7acOD2bT7iBn70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJz3gC/dJMcag6bTVW/N1jBm0iM7acOD2bT7iBn70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJz3gC%2FdJMcag6bTVW%2FN1jBm0iM7acOD2bT7iBn70%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;309&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;268&quot; data-start=&quot;216&quot; data-ke-size=&quot;size16&quot;&gt;즉, 배치의 구조를 제대로 사용하기보다는 동작 자체를 이해하는 데에 초점을 맞춘 상태였다. 하지만 스프링 배치에서는 이러한 작업 흐름을&lt;br /&gt;Job과 Step이라는 개념을 통해 구조적으로 관리할 수 있다. 단순 실행에서 벗어나, 실행 단위와 흐름을 명확하게 정의할 수 있는 것이다.&lt;br /&gt;그렇다면 스프링 배치에서는 이 Job과 Step을 어떻게 구성하고 사용하는지 확인해보자. 그리고 저번시간에 재대로 설명하지 않은 chunk도 함께 알아보자.&lt;/p&gt;
&lt;h2 data-end=&quot;268&quot; data-start=&quot;216&quot; data-ke-size=&quot;size26&quot;&gt;그렇다면, Step이란 무엇일까?&lt;/h2&gt;
&lt;p data-end=&quot;268&quot; data-start=&quot;216&quot; data-ke-size=&quot;size16&quot;&gt;사실 지금까지 작성했던 코드를 보면 이미 Step과 비슷한 구조로 동작하고 있었다.&lt;br /&gt;Reader &amp;rarr; Processor &amp;rarr; Writer 흐름을 직접 이어서 실행하고 있었기 때문이다.&lt;br /&gt;즉, 위에서 보였던 코드는 Step을 &quot;흉내낸 코드&quot;라고 볼 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;246&quot; data-start=&quot;207&quot; data-ke-size=&quot;size16&quot;&gt;하지만 스프링 배치에서의 Step은 단순히 흐름을 묶는 수준이 아니다.&lt;br /&gt;Step은 배치 처리의 &quot;실행 단위&quot;이다. Reader, Processor, Writer를 하나로 묶어서 실제로 동작시키는 역할을 한다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;조금 더 정확하게 말하면 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Step은 다음과 같은 것들을 관리한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;457&quot; data-start=&quot;368&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;405&quot; data-start=&quot;368&quot; data-section-id=&quot;yfyo4t&quot;&gt;Reader &amp;rarr; Processor &amp;rarr; Writer 흐름 실행&lt;/li&gt;
&lt;li data-end=&quot;417&quot; data-start=&quot;406&quot; data-section-id=&quot;11mio6t&quot;&gt;트랜잭션 관리&lt;/li&gt;
&lt;li data-end=&quot;441&quot; data-start=&quot;418&quot; data-section-id=&quot;1181gyf&quot;&gt;반복 처리 (chunk 기반 처리)&lt;/li&gt;
&lt;li data-end=&quot;457&quot; data-start=&quot;442&quot; data-section-id=&quot;87mu7q&quot;&gt;실패 시 재시도 및 롤백&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;513&quot; data-start=&quot;459&quot; data-ke-size=&quot;size16&quot;&gt;즉, 우리가 직접 구현했던 단순한 흐름에 &quot;운영에 필요한 기능&quot;이 추가된 구조라고 보면 된다. &lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그래서 이전까지는 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&quot;동작을 이해하기 위해 직접 연결해서 실행했다&quot;라면 &lt;br /&gt;&lt;/span&gt;이제부터는 &quot;스프링 배치가 제공하는 Step을 통해 구조적으로 실행한다&quot;라고 보면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1048&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pC0Ya/dJMcafGdyqs/Z1gIxV0PiR7I7uabOSZgc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pC0Ya/dJMcafGdyqs/Z1gIxV0PiR7I7uabOSZgc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pC0Ya/dJMcafGdyqs/Z1gIxV0PiR7I7uabOSZgc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpC0Ya%2FdJMcafGdyqs%2FZ1gIxV0PiR7I7uabOSZgc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;577&quot; height=&quot;409&quot; data-origin-width=&quot;1048&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면, Step이 많아지면 어떻게 되어질까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답은 생각보다 단순하다. 여러 개의 Step을 하나로 묶어서 관리하게 된다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;u&gt;이때 등장하는 개념이 바로 Job&lt;/u&gt;이다. &lt;/span&gt;Job은 여러 개의 Step을 하나의 흐름으로 구성한 상위 개념이다.&lt;br /&gt;즉, Step이 하나의 작업 단위라면 Job은 그 작업들을 순서대로 실행하는 &quot;전체 흐름&quot;이라고 보면 된다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어, 다음과 같은 상황을 생각해볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;331&quot; data-start=&quot;278&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;295&quot; data-start=&quot;278&quot; data-section-id=&quot;1aof7h5&quot;&gt;Step1: 데이터 조회&lt;/li&gt;
&lt;li data-end=&quot;313&quot; data-start=&quot;296&quot; data-section-id=&quot;lo9jir&quot;&gt;Step2: 데이터 가공&lt;/li&gt;
&lt;li data-end=&quot;331&quot; data-start=&quot;314&quot; data-section-id=&quot;6ws9du&quot;&gt;Step3: 데이터 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;396&quot; data-start=&quot;333&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 여러 개의 Step이 존재할 경우, 이 Step들을 순서대로 실행하도록 묶어주는 것이 바로 Job이다.&lt;br /&gt;즉 구조를 정리하면 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775828238004&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Job &amp;rarr; Step &amp;rarr; (Reader &amp;rarr; Processor &amp;rarr; Writer)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;481&quot; data-start=&quot;462&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 계층 구조로 이루어져 있다. 결국 Step이 하나일 때는 단순한 배치 작업이지만,&lt;br /&gt;Step이 여러 개가 되면 하나의 흐름으로 관리해야 할 필요가 생기고 그 역할을 Job이 담당하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0p8lX/dJMcaaEVqFb/L8KUQLSKWxky53xPol8D0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0p8lX/dJMcaaEVqFb/L8KUQLSKWxky53xPol8D0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0p8lX/dJMcaaEVqFb/L8KUQLSKWxky53xPol8D0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0p8lX%2FdJMcaaEVqFb%2FL8KUQLSKWxky53xPol8D0K%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;768&quot; height=&quot;191&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 배치의 모든 과정을 하나의 Step으로 만들었기때문에 Job에 들어가는 Step하나 뿐이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JobOperator&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 Job과 Step을 잘 나누어두었다고 하더라도, 결국 이것을 실제로 실행시켜줄 실행기가 필요하다.&lt;br /&gt;물론 처음 보여줬던 코드처럼 직접 호출하는 방식으로 작성할 수도 있다. 동작만 놓고 보면 그렇게 작성해도 큰 문제는 없다. &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJz3gC/dJMcag6bTVW/N1jBm0iM7acOD2bT7iBn70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJz3gC/dJMcag6bTVW/N1jBm0iM7acOD2bT7iBn70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJz3gC/dJMcag6bTVW/N1jBm0iM7acOD2bT7iBn70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJz3gC%2FdJMcag6bTVW%2FN1jBm0iM7acOD2bT7iBn70%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;309&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;604&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-end=&quot;382&quot; data-start=&quot;280&quot; data-ke-size=&quot;size16&quot;&gt;처음 방식은 개발자가 직접 실행 흐름을 제어하는 구조이다. 즉, 스프링 배치의 실행 구조 안에서 동작하는 것이 아니라,&lt;br /&gt;우리가 강제로 호출해서 실행시키는 코드라고 볼 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;452&quot; data-start=&quot;384&quot; data-ke-size=&quot;size16&quot;&gt;다시 말해, 이 코드는 스프링이 관리하는 배치 코드라기보다는, 배치와 비슷한 흐름을 직접 구현한 코드에 가깝다.&lt;/p&gt;
&lt;p data-end=&quot;630&quot; data-start=&quot;558&quot; data-ke-size=&quot;size16&quot;&gt;반면 스프링 배치에서는 JobOperator와 같은 실행 구조를 통해&lt;br /&gt;스프링이 Job을 인식하고, 정해진 방식에 따라 실행하고 관리할 수 있도록 한다.&lt;/p&gt;
&lt;p data-end=&quot;267&quot; data-start=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;즉 핵심은 단순하다.&lt;/p&gt;
&lt;p data-end=&quot;341&quot; data-start=&quot;269&quot; data-ke-size=&quot;size16&quot;&gt;직접 실행하는 방식은 &quot;배치처럼 보이는 코드&quot;이고, 스프링이 관리하는 방식은 &quot;실제로 스프링 배치 위에서 동작하는 코드&quot;이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;974&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvTtHV/dJMcadO56NH/TYW7UFlLM1E7IvMyrBcdgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvTtHV/dJMcadO56NH/TYW7UFlLM1E7IvMyrBcdgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvTtHV/dJMcadO56NH/TYW7UFlLM1E7IvMyrBcdgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvTtHV%2FdJMcadO56NH%2FTYW7UFlLM1E7IvMyrBcdgk%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;645&quot; height=&quot;407&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;974&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Chunk&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chunk는 데이터를 일정 단위로 끊어서 처리하는 역할을 한다.&lt;br /&gt;한 번에 너무 많은 데이터가 들어오게 되면, 처리 중 실패할 가능성이 높아진다.&lt;/p&gt;
&lt;p data-end=&quot;220&quot; data-start=&quot;180&quot; data-ke-size=&quot;size16&quot;&gt;그래서 배치에서는 데이터를 일정 크기로 나누어 처리하는 방식을 사용한다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 10,000개의 데이터를 한 번에 저장하는 것보다, &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;100개씩 나누어 100번 저장하는 방식이 더 안정적 일 수 있다.&lt;br /&gt;&lt;/span&gt;이렇게 하면 중간에 실패하더라도 전체가 아닌 일부만 다시 처리하면 되기 때문에, 복구와 재처리 측면에서도 유리하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-end=&quot;220&quot; data-start=&quot;180&quot; data-ke-size=&quot;size16&quot;&gt;배치에 대해 기본적으로 학습해야 하는 부분은 완료가 되어졌다고 느껴진다. 하지만 이것보다 깊게 들어가는 작업은 아직 진행하지는 않았습니다. 예를 들면, 기존에는 JobLaucher이 존재하였습니다. 하지만 spring 6.0이후부터는 JobOperator를 사용하도록 권장하고 있습니다. 어째서 JobLauncher는 더 이상 사용하지 않는지, 두 개의 차이점은 무엇이며, JobOperator의 장점이 어떤게 있는지 상세하게 파볼 예정입니다. 또, 현재는 스케쥴방식이 아닌 수동으로 배치를 돌리고 있습니다. 스케쥴 방식으로 바꾸게 되면 어떤 문제가 있는지도 살펴보면 좋을거 같네요. 다음 배치는 조금더 깊게 들어가는 시간을 가져보겠습니다. 긴 글 읽어주셔서 감사합니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/568</guid>
      <comments>https://b-programmer.tistory.com/568#entry568comment</comments>
      <pubDate>Fri, 10 Apr 2026 23:05:42 +0900</pubDate>
    </item>
    <item>
      <title>Batch - processor, writer</title>
      <link>https://b-programmer.tistory.com/567</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저번시간에 간단하게 배치가 어떤것인지 그리고 Reader에 대해 짧게 학습을 진행하였다. 간단하게 복습하면 Reader은 어딘가에 쌓여있는 데이터를 읽는 작업을 일컫는다. 하지만 배치의 전 과정을 생각해보면, 단순히 읽는 것으로 끝나지 않는다. 데이터를 읽고, 가공하고, 저장하는 흐름으로 이루어져 있다. 그렇다면, 어째서 이러한 과정을 거칠까? 이유는 생각보다 단순하다.&lt;/p&gt;
&lt;p data-end=&quot;393&quot; data-start=&quot;254&quot; data-ke-size=&quot;size16&quot;&gt;첫 번째는 데이터의 크기 때문이다. 대량의 데이터를 한 번에 처리하려고 하면 메모리 부담이 커지고, 중간에 실패했을 때 다시 처음부터 실행해야 하는 문제가 발생한다. 그래서 배치는 데이터를 일정 단위로 나누어 끊어서 처리하는 방식을 선택한다.&lt;/p&gt;
&lt;p data-end=&quot;534&quot; data-start=&quot;395&quot; data-ke-size=&quot;size16&quot;&gt;두 번째는 데이터의 구조가 서로 다르기 때문이다. 어딘가에 쌓여있는 데이터의 구조와 DB에 저장되어 있는 데이터 구조는 대부분 동일하지 않다. 그렇기 때문에 읽어온 데이터를 그대로 저장하는 것이 아니라, 목적에 맞게 변환하는 과정이 필요하다.&lt;/p&gt;
&lt;p data-end=&quot;590&quot; data-start=&quot;536&quot; data-ke-size=&quot;size16&quot;&gt;이러한 흐름 때문에 배치는 Reader, Processor, Writer라는 구조를 가지게 된다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이번에는 Processor와 Writer를 진행해볼 예정이다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;작업을 해봤는데 생각보다 크기가 크지 않아 두 개를 나눠서 설명하기보다는 하나의 흐름으로 묶어서 설명하려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;590&quot; data-start=&quot;536&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;데이터 가공을 시작해보자.&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;위에서 데이터 가공을 왜 하는지 간략하게 살펴봤다.&lt;br /&gt;그렇다면 스프링 배치에서는 어떻게 데이터 가공할 수 있을까?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;1206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqCfV7/dJMcafsD6qz/id2b5KZNWRSsBxG7cIkvCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqCfV7/dJMcafsD6qz/id2b5KZNWRSsBxG7cIkvCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqCfV7/dJMcafsD6qz/id2b5KZNWRSsBxG7cIkvCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqCfV7%2FdJMcafsD6qz%2Fid2b5KZNWRSsBxG7cIkvCk%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;406&quot; height=&quot;359&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;1206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reader에서 ItemReader라는 것을 본 적이 있다. Reader에서는 분명 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;implements ItemReader&amp;lt;PriceReadItem&amp;gt;&lt;/span&gt; 이렇게 되어 있었다. 그런데 ItemProcessor를 확인해보면 조금 다르다. PriceReadItem뿐만 아니라 PriceData까지 같이 선언되어 있는 것을 확인할 수 있다.&lt;br /&gt;&lt;br /&gt;이 부분에서 헷갈릴 수 있는데, 구조는 생각보다 단순하다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ItemProcessor&amp;lt;PriceReadItem, PriceData&amp;gt;&lt;/span&gt;는 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Reader에서 읽어온 PriceReadItem을 받아서,&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;PriceData 형태로 변환해서 넘긴다는 의미이다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, 입력 타입과 출력 타입이 다르다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이게 바로 Processor의 핵심이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;394&quot; data-start=&quot;281&quot; data-ke-size=&quot;size16&quot;&gt;놀랍게도 이게 전부다. 물론 여기에 추가적으로 특정 데이터는 필요 없으니 제거하거나, 서비스에 맞게 값을 가공하거나, 형태를 조금 더 보기 좋게 바꾸는 작업이 들어갈 수도 있다. 하지만 모든 서비스에서 복잡한 가공이 필요한 것은 아니다.&lt;/p&gt;
&lt;p data-end=&quot;657&quot; data-start=&quot;586&quot; data-ke-size=&quot;size16&quot;&gt;경우에 따라서는 단순히 DTO &amp;rarr; Entity처럼 매핑만 수행해도 Processor의 역할은 충분히 끝났다고 봐도 무방하다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;결국 Processor의 본질은 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&quot;읽어온 데이터를 다음 단계에서 사용할 수 있는 형태로 바꿔주는 것&quot;이다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가공한 데이터를 사용해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가공한 데이터를 사용하는 방법도 크게 틀리지는 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgu4sr/dJMcabDNx87/P4geQRKihDA1kj0LccyvWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgu4sr/dJMcabDNx87/P4geQRKihDA1kj0LccyvWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgu4sr/dJMcabDNx87/P4geQRKihDA1kj0LccyvWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgu4sr%2FdJMcabDNx87%2FP4geQRKihDA1kj0LccyvWK%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;532&quot; height=&quot;327&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 chunk라는 단위가 등장한다. 하지만 현재 나는 chunk를 사용하지 않고 배치를 실행하고 있다.&lt;br /&gt;그 이유는 단순하다. 배치의 동작을 조금 더 명확하게 이해하기 위해서이다.&lt;/p&gt;
&lt;p data-end=&quot;279&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;chunk를 사용하게 되면 내부적으로 묶어서 처리되기 때문에, 데이터가 어떻게 흘러가는지 직관적으로 파악하기 어렵다고 느꼈다.&lt;br /&gt;그래서 현재는 chunk 없이 하나씩 처리하는 방식으로 동작을 확인하고 있다.&lt;/p&gt;
&lt;p data-end=&quot;395&quot; data-start=&quot;281&quot; data-ke-size=&quot;size16&quot;&gt;현재 Writer에서는 RDB에 데이터를 저장하고 있다. 레디스에 넣든, 로그로 찍든, 파일로 만들든 그것은 크게 중요하지 않다.&lt;br /&gt;결국 Writer의 역할은 &quot;어딘가에 기록한다&quot;는 점에서 동일하다.&lt;br /&gt;&lt;br /&gt;여기서 중요한 포인트는 따로 있다. 바로 chunk 단위로 데이터가 처리된다는 점이다.&lt;br /&gt;이제 한번 실행해보자. 현재는 스케줄링이 아닌, 수동으로 배치를 실행하고 있는 상태이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QAHIj/dJMcabDNykx/K840ycXsEUtyLkGkrltTo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QAHIj/dJMcabDNykx/K840ycXsEUtyLkGkrltTo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QAHIj/dJMcabDNykx/K840ycXsEUtyLkGkrltTo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQAHIj%2FdJMcabDNykx%2FK840ycXsEUtyLkGkrltTo1%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;1774&quot; height=&quot;368&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인을 해보면, 약 230개의 데이터가 추가적으로 쌓이고 있는 것을 확인할 수 있다. 이 과정을 통해 Reader &amp;rarr; Processor &amp;rarr; Writer까지의 전체 흐름이 정상적으로 동작한다는 것을 확인할 수 있었다. 이렇게 해서 배치의 기본 구성 요소인 Reader, Processor, Writer를 모두 살펴보았다. 하지만 그렇다고 해서 배치에 대한 학습이 끝난 것은 아니다.&lt;/p&gt;
&lt;p data-end=&quot;790&quot; data-start=&quot;721&quot; data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 배치를 구성하는 핵심 개념인 Job, Step, 그리고 Chunk에 대해 조금 더 자세하게 알아보려고 한다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/567</guid>
      <comments>https://b-programmer.tistory.com/567#entry567comment</comments>
      <pubDate>Thu, 9 Apr 2026 22:58:01 +0900</pubDate>
    </item>
    <item>
      <title>카프카의 설정은 진짜일까?? - offset(1)</title>
      <link>https://b-programmer.tistory.com/566</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;원래 시나리오대로면, producer를 전부 끝내고 나서 consumer를 학습하려고 하였지만 살짝 우회하기로 결정하였습니다. 그 이유가 뭐냐면 카프카의 주요 특징중 하나가 offset으로 알고 있습니다. 제가 알기로 offset은 카프카(브로커)에서 값을 읽을때 특정 부분까지 읽도록 하는걸로 알고 있습니다. 물론 이게 틀린내용일 수 도 있지만, 뭐 상관없습니다. 다시 제대로 학습하면 되는거니까요. 시작해봅시다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 offset이 뭐냐면..&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset은 메시지의 위치를 나타내는 번호입니다. kafkasms 메시지를 다음과 같이 저장합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1775653989213&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[0] [1] [2] [3] [4] ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;207&quot; data-start=&quot;186&quot; data-ke-size=&quot;size16&quot;&gt;여기서 숫자가 바로 offset입니다. 즉, kafka는 메시지를 순서대로 쌓고, index처럼 offset을 붙입니다.&lt;br /&gt;또한, kafka는 메시지를 삭제하지 않습니다. 대신 어디까지 읽었는지만 관리합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;207&quot; data-start=&quot;186&quot; data-ke-size=&quot;size23&quot;&gt;그렇다면, offset은 어떻게 관리가 되어질까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset은 기본적으로 kafka에서 관리하지만, 언제 기록할지는 consumer가 결정하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcQBfG/dJMcajhsWtK/PknvqBO6NYGzKIZ6Sx52JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcQBfG/dJMcajhsWtK/PknvqBO6NYGzKIZ6Sx52JK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcQBfG/dJMcajhsWtK/PknvqBO6NYGzKIZ6Sx52JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcQBfG%2FdJMcajhsWtK%2FPknvqBO6NYGzKIZ6Sx52JK%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;794&quot; height=&quot;156&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림을 이렇게 그릴 수 있습니다. kafka의 구조를 이해할 때, kafka는 곧 broker라고 볼 수 있습니다.&lt;br /&gt;즉, kafka는 메시지를 저장하고 관리하는 서버 역할을 수행합니다.&lt;/p&gt;
&lt;p data-end=&quot;222&quot; data-start=&quot;142&quot; data-ke-size=&quot;size16&quot;&gt;따라서 kafka를 이야기할 때는 단일 애플리케이션이라기보다는 메시지를 저장하는 Broker의 집합으로 이해하는 것이 더 적절합니다.&lt;br /&gt;반면에 producer와 consumer는 kafka 내부에 포함된 요소가 아니라, kafka와 통신하는 별도의 애플리케이션입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;381&quot; data-start=&quot;301&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;338&quot; data-start=&quot;301&quot; data-section-id=&quot;pjfyjm&quot;&gt;producer는 메시지를 kafka로 전송하는 역할을 하고&lt;/li&gt;
&lt;li data-end=&quot;381&quot; data-start=&quot;339&quot; data-section-id=&quot;19su8iw&quot;&gt;consumer는 kafka에 저장된 메시지를 읽어가는 역할을 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;489&quot; data-start=&quot;383&quot; data-ke-size=&quot;size16&quot;&gt;정리하면, kafka는 데이터를 저장하는 broker이고, &lt;br /&gt;producer와 consumer는 그 데이터를 주고받기 위해 kafka와 상호작용하는 외부 애플리케이션이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;489&quot; data-start=&quot;383&quot; data-ke-size=&quot;size16&quot;&gt;하지만 여기서 한 가지 이상한 점이 있습니다. offset은 kafka에서 메시지를 순서대로 저장하면서,&lt;br /&gt;각 메시지에 index처럼 붙는 값이라고 알고 있습니다. 즉, broker가 관리하는 값이라고 볼 수 있습니다.&lt;br /&gt;그런데 실제로 동작을 살펴보면, offset의 흐름은 오히려 consumer가 주도하는 것처럼 느껴집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;346&quot; data-start=&quot;278&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;309&quot; data-start=&quot;278&quot; data-section-id=&quot;6hk9dl&quot;&gt;어디까지 읽을지 결정하는 것도 consumer이고&lt;/li&gt;
&lt;li data-end=&quot;346&quot; data-start=&quot;310&quot; data-section-id=&quot;bkx1ua&quot;&gt;언제 offset을 기록할지도 consumer가 결정합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;372&quot; data-start=&quot;348&quot; data-ke-size=&quot;size16&quot;&gt;이쯤 되면 자연스럽게 이런 의문이 생깁니다. &lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;offset은 kafka(Broker)의 것일까요, &lt;/span&gt;아니면 consumer의 것일까요?&lt;/p&gt;
&lt;h4 data-end=&quot;465&quot; data-start=&quot;435&quot; data-section-id=&quot;1lkc5s&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;핵심은 소유가 아니라 역할 분리입니다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;521&quot; data-start=&quot;467&quot; data-ke-size=&quot;size16&quot;&gt;이 질문의 답은 둘 중 하나가 아니라, 역할이 나뉘어 있다고 보는 것이 더 정확합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;597&quot; data-start=&quot;523&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;551&quot; data-start=&quot;523&quot; data-section-id=&quot;4ym5uu&quot;&gt;offset 자체는 broker에 저장됩니다&lt;/li&gt;
&lt;li data-end=&quot;597&quot; data-start=&quot;552&quot; data-section-id=&quot;9jiwyr&quot;&gt;하지만 offset을 언제, 어디까지 올릴지는 consumer가 결정합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;601&quot; data-start=&quot;599&quot; data-ke-size=&quot;size16&quot;&gt;즉, offset은 kafka가 보관하고, consumer가 정의하는 값입니다.&lt;/p&gt;
&lt;p data-end=&quot;601&quot; data-start=&quot;599&quot; data-ke-size=&quot;size16&quot;&gt;좋습니다. consumer가 offset을 정의한다고 하였는데 왜 정의를 하는걸까요? 이미 broker에 순서가 저장이 되었는데 말이죠.&lt;br /&gt;broker에서 메시지를 순서대로 쌓고 offset을 붙입니다. 여기까지는 데이터를 저장하는 역할입니다.&lt;br /&gt;하지만 여기에는 한 가지 중요한 정보가 빠져잇습니다. 이 메시지는 처리가 되었는가 라는 정보입니다.&lt;/p&gt;
&lt;p data-end=&quot;601&quot; data-start=&quot;599&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 다음과 같이 정리할 수 있습니다. broker는 메시지를 저장하는 역할을 하고,&lt;br /&gt;consumer는 해당 메시지를 실제로 처리하는 역할을 수행합니다.&lt;br /&gt;이 둘은 단순히 데이터를 주고받는 관계가 아니라, &quot;저장&quot;과 &quot;처리&quot;라는 서로 다른 책임을 가진 구조입니다.&lt;br /&gt;그렇다면 consumer는 메시지를 수신한 이후, 자신의 처리 상태를 broker에게 알려줄 필요가 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;601&quot; data-start=&quot;599&quot; data-ke-size=&quot;size16&quot;&gt;왜냐하면 broker는 메시지를 쌓아두기만 할 뿐, 해당 메시지가 실제로 처리되었는지에 대해서는 알 수 없기 때문입니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;따라서 consumer는 다음과 같이 동작합니다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&quot;나는 이 메시지들(offset 기준)을 이미 처리했습니다.&quot;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 정보를 Broker에 전달하는 행위가 바로 &lt;/span&gt;offset commit&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;601&quot; data-start=&quot;599&quot; data-ke-size=&quot;size23&quot;&gt;그렇다면, 저장과 처리를 구분하는 이유가 무엇일까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 앞서 말했듯이, broker와 consumer가 서로 다른 애플리케이션으로 동작하기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;265&quot; data-start=&quot;169&quot; data-ke-size=&quot;size16&quot;&gt;broker는 메시지를 저장하고 offset을 부여하는 역할만 수행합니다. 하지만 해당 메시지가 실제로 사용되었는지,&lt;br /&gt;즉 처리되었는지 여부는 알 수 없습니다. 이 말은 곧, 이런 상황을 의미합니다. &lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;broker는 메시지를 전달할 수는 있지만, &lt;/span&gt;그 메시지가 성공적으로 처리되었는지는 책임지지 않는습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;따라서 Consumer는 단순히 메시지를 읽는 것에서 끝나는 것이 아니라, &lt;/span&gt;자신이 어디까지 처리했는지를 명확하게 알려줄 필요가 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;444&quot; data-start=&quot;442&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&quot;나는 여기까지(offset 기준) 처리 완료했습니다&quot; &lt;/span&gt;라는 상태를 Broker에 전달해야 합니다.&lt;br /&gt;이 과정을 통해서만 kafka는 이후에 어디부터 다시 읽어야 하는지를 판단할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-end=&quot;190&quot; data-start=&quot;156&quot; data-ke-size=&quot;size16&quot;&gt;이 행위를 offset을 commit한다고 표현합니다. 즉, consumer가 처리한 위치를 기록하여 이후에는 해당 지점 이후부터 메시지를 읽을 수 있도록 만드는 과정입니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;다음 시간에는 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;offset이 Broker에 실제로 어떻게 저장되는지,&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그리고 offset commit이 어떤 방식으로 이루어지는지 &lt;/span&gt;이 두 가지를 중심으로 살펴보도록 하겠습니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/566</guid>
      <comments>https://b-programmer.tistory.com/566#entry566comment</comments>
      <pubDate>Wed, 8 Apr 2026 23:27:21 +0900</pubDate>
    </item>
    <item>
      <title>Batch- Reader</title>
      <link>https://b-programmer.tistory.com/565</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저번 글에서는 스프링 배치를 왜 사용하는지에 대해 살펴보았다. 일괄 작업이 필요한 경우, 배치는 효과적인 선택지가 될 수 있다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;배치의 기본 흐름은 단순하다. 데이터를 읽고, 가공한 뒤, 저장하는 것이다. &lt;/span&gt;데이터의 출처가 어디든 상관없이, 해당 위치에서 데이터를 읽어오고, 배치 애플리케이션 내부에서 가공한 뒤, 최종 결과를 저장한다. 여기서 중요한 특징 중 하나는 배치가 주로 스케줄링 기반으로 실행된다는 점이다. 즉, 정해진 시간에 자동으로 실행되며, 사용자 요청과는 분리되어 동작한다.&lt;/p&gt;
&lt;p data-end=&quot;424&quot; data-start=&quot;331&quot; data-ke-size=&quot;size16&quot;&gt;이 말은 곧, 배치가 처리하는 작업은 실시간으로 즉시 반영될 필요가 없으며, 대량의 데이터를 나누어 처리하더라도 서비스에 직접적인 영향을 주지 않는다는 의미다. 따라서 배치는 뒤에서 안정적으로 대량 데이터를 처리하는 데 적합한 방식이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;544&quot; data-start=&quot;482&quot; data-ke-size=&quot;size16&quot;&gt;이제부터는 스프링 배치에 대해 본격적으로 학습해보려고 한다. 첫 번째로 살펴볼 것은 Reader이다.&lt;/p&gt;
&lt;h2 data-end=&quot;544&quot; data-start=&quot;482&quot; data-ke-size=&quot;size26&quot;&gt;Reader의 역할은 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reader의 역할은 단순하다. 데이터를 가져오는 것이다.&lt;br /&gt;좀 더 정확하게 말하면, 어딘가에 쌓여 있는 데이터를 하나씩 꺼내오는 것을 의미한다.&lt;/p&gt;
&lt;p data-end=&quot;191&quot; data-start=&quot;138&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 데이터를 하나씩 처리할까? 배치는 대량의 데이터를 처리하는 방식이라고 배워왔다.&lt;/p&gt;
&lt;p data-end=&quot;277&quot; data-start=&quot;193&quot; data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은, 수천 건, 수만 건의 데이터를 한 번에 모두 가져오게 되면&lt;br /&gt;메모리 부담이 커지고, 처리 흐름을 관리하기도 어려워진다는 것이다.&lt;/p&gt;
&lt;p data-end=&quot;371&quot; data-start=&quot;279&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Reader는 데이터를 한 건씩, 혹은 스트림처럼 순차적으로 읽어온다.&lt;br /&gt;이 방식은 메모리 사용을 줄이고, 대량 데이터를 안정적으로 처리할 수 있게 해준다.&lt;/p&gt;
&lt;p data-end=&quot;455&quot; data-start=&quot;373&quot; data-ke-size=&quot;size16&quot;&gt;또한 배치는 실시간 처리가 목적이 아니기 때문에,&lt;br /&gt;즉시 결과를 확인하기보다는 전체 데이터를 끝까지 안정적으로 처리하는 것이 더 중요하다.&lt;br /&gt;정리하면, Reader의 역할은 다음과 같다. 어딘가에 저장된 대량의 데이터를, 순차적으로 하나씩 읽어와 다음 단계로 전달하는 것이다.&lt;/p&gt;
&lt;h2 data-end=&quot;455&quot; data-start=&quot;373&quot; 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;1444&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cahMdi/dJMb99MGl2y/7vkkbGd2NFAk28qkZnTFEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cahMdi/dJMb99MGl2y/7vkkbGd2NFAk28qkZnTFEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cahMdi/dJMb99MGl2y/7vkkbGd2NFAk28qkZnTFEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcahMdi%2FdJMb99MGl2y%2F7vkkbGd2NFAk28qkZnTFEK%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;652&quot; height=&quot;301&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 외부 api를 통해 데이터를 가져오고 있다. 여기서 의문이 드는 사실이 있다.&lt;br /&gt;데이터를 그냥 가져와서 저장하면 되는것이 아닌가 라는 생각이 들 수 있다.&lt;br /&gt;&lt;br /&gt;이유는 다음과 같다.&amp;nbsp;&lt;br /&gt;데이터는 항상 그대로 쓸 수 없다. 외부 API에서 가져온 데이터는 우리 서비스의 데이터 구조와 다를 수 있고, 불필요한 값이 포함되어 있을 수 있으며, 추가적인 계산이나 변환이 필요할 수 도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;1014&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E13G7/dJMcacQcOB9/ku4jyyDgJywmvK2KyYhiQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E13G7/dJMcacQcOB9/ku4jyyDgJywmvK2KyYhiQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E13G7/dJMcacQcOB9/ku4jyyDgJywmvK2KyYhiQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE13G7%2FdJMcacQcOB9%2Fku4jyyDgJywmvK2KyYhiQ1%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;311&quot; height=&quot;494&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;1014&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막상 이 데이터를 쓰기에는 뭔가 애매하다는 생각이 들 수 있다. 즉, 단순 저장이 아니라 가공 과정이 반드시 필요하다.&lt;br /&gt;또, 가져온 데이터를 바로 변환하면 되지 않나 생각할 수 있다. 하지만, 대량 처리 시 안정성이 중요하다.&lt;br /&gt;데이터가 많아질수록 중간에 실패할 수 있고 일부만 저장될 수 있으며 재처리가 필요할 수 있다.&lt;br /&gt;이때 단순 저장 구조로는 대응이 어렵다. 다른 이유도 있겠지만 지금 파트는 Reader이기 때문에 이쯤하고 넘어가겠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 어떻게, 데이터를 가져올까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 예시는 외부 API를 통해 데이터를 가져오는 방식이었다. 하지만 지금부터 살펴볼 내용은, 외부 API의 결과를 단순히 사용하는 것이 아니라, 스프링 배치에서 데이터를 읽어오는 방식에 대해 이야기해보려고 한다.&lt;/p&gt;
&lt;p data-end=&quot;338&quot; data-start=&quot;270&quot; data-ke-size=&quot;size16&quot;&gt;즉, 데이터의 출처가 어디인지보다 중요한 것은 배치 내부에서 이 데이터를 어떤 방식으로 읽어오고 처리하느냐이다.&lt;br /&gt;이러한 관점에서, 스프링 배치는 데이터를 읽기 위해 ItemReader라는 개념을 제공한다.&lt;br /&gt;이제 이 ItemReader를 통해 데이터를 어떻게 읽어오는지 살펴보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;ItemReader은 무엇인가?&lt;/h3&gt;
&lt;p data-end=&quot;808&quot; data-start=&quot;765&quot; data-ke-size=&quot;size16&quot;&gt;ItemReader는 스프링 배치에서 데이터를 읽기 위한 인터페이스이다.&lt;br /&gt;배치 처리의 첫 단계에서 동작하며, 어딘가에 저장되어 있는 데이터를 하나씩 가져오는 역할을 한다.&lt;/p&gt;
&lt;p data-end=&quot;233&quot; data-start=&quot;159&quot; data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은, ItemReader는 데이터를 가공하거나 저장하지 않는다는 것이다. 오직 데이터를 읽어오는 것에만 집중한다.&lt;br /&gt;또한 ItemReader는 데이터를 한 번에 모두 가져오는 것이 아니라, read() 메서드를 통해 한 건씩 순차적으로 반환하는 방식으로 동작한다.&lt;br /&gt;이러한 구조 덕분에, 대량의 데이터를 처리하더라도 메모리 사용을 최소화하면서 안정적으로 작업을 수행할 수 있다.&lt;br /&gt;정리하면, ItemReader는 배치에서 데이터를 읽어오는 시작점이자, 데이터를 하나씩 다음 단계로 전달하는 역할을 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;1026&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boDeGU/dJMcagd03II/vRsomDFXS3c3IUv9C0P4R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boDeGU/dJMcagd03II/vRsomDFXS3c3IUv9C0P4R0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boDeGU/dJMcagd03II/vRsomDFXS3c3IUv9C0P4R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboDeGU%2FdJMcagd03II%2FvRsomDFXS3c3IUv9C0P4R0%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;623&quot; height=&quot;446&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;1026&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;320&quot; data-start=&quot;248&quot; data-ke-size=&quot;size16&quot;&gt;배치는 대량의 데이터를 처리하는 방식이다.&lt;br /&gt;수천 건, 수만 건의 데이터를 한 번에 모두 메모리에 올려 처리하게 되면, 메모리 부담이 커지고 처리 흐름을 제어하기도 어려워진다.&lt;br /&gt;이러한 이유로 Reader는 데이터를 한 번에 모두 가져오는 것이 아니라, 필요한 시점에 하나씩 꺼내어 다음 단계로 전달하는 방식으로 동작한다. 즉, Reader는 단순히 데이터를 조회하는 것이 아니라, 처리 흐름에 맞게 데이터를 공급하는 역할을 수행한다고 볼 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;375&quot; data-start=&quot;339&quot; data-ke-size=&quot;size16&quot;&gt;이제 이 Reader를 실제로 어떻게 사용할 수 있는지 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgDSho/dJMcadO3o8N/8JubWQWgd7K8jT8MX7MX1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgDSho/dJMcadO3o8N/8JubWQWgd7K8jT8MX7MX1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgDSho/dJMcadO3o8N/8JubWQWgd7K8jT8MX7MX1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgDSho%2FdJMcadO3o8N%2F8JubWQWgd7K8jT8MX7MX1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;273&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ItemReader를 통해 데이터를 하나씩 읽는 방식과, 외부 API를 통해 데이터를 가져오는 방식은 이미 살펴보았다.&lt;br /&gt;하지만 아직 이 두 가지가 어떻게 연결되는지는 살펴보지 않았다. 현재 코드에서는 이 연결 지점에서 ListItemReader 를 사용하고 있다.&lt;br /&gt;즉, 외부 API를 통해 조회한 데이터를 리스트로 만든 뒤, ListItemReader가 이를 하나씩 꺼내어 ItemReader처럼 동작하도록 구성하고 있는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면, 이렇게 데이터를 가져오는 방법은 ListItemReader만 존재할까?&lt;/h3&gt;
&lt;p data-end=&quot;91&quot; data-start=&quot;81&quot; data-ke-size=&quot;size16&quot;&gt;물론 그렇지 않다.&lt;br /&gt;스프링 배치는 다양한 데이터 소스에 맞게 여러 종류의 ItemReader를 제공하고 있다.&lt;br /&gt;ListItemReader는 그 중 하나일 뿐이며, 상황에 따라 더 적합한 Reader를 선택할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;343&quot; data-start=&quot;211&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 데이터베이스에서 직접 조회하는 경우에는 JdbcCursorItemReader나 JdbcPagingItemReader를 사용할 수 있고,&lt;br /&gt;파일을 읽는 경우에는 FlatFileItemReader를 사용할 수 있다. 즉, ListItemReader는 외부에서 이미 수집된 데이터를 처리할 때 유용한 방식이며, 데이터의 출처와 특성에 따라 다양한 Reader를 선택할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;343&quot; data-start=&quot;211&quot; data-ke-size=&quot;size16&quot;&gt;이에 대해서는 나중에 다시 정리해보자.&lt;/p&gt;
&lt;h2 data-end=&quot;343&quot; data-start=&quot;211&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치의 첫 단계인 Reader에 대해 살펴보았다. 데이터가 어디에서 들어오고, 어떤 방식으로 읽히는지에 대해 학습해보았다.&lt;br /&gt;다음 시간에는 Processor에 대해 알아보자.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/565</guid>
      <comments>https://b-programmer.tistory.com/565#entry565comment</comments>
      <pubDate>Tue, 7 Apr 2026 23:43:02 +0900</pubDate>
    </item>
    <item>
      <title>인덱스 정리</title>
      <link>https://b-programmer.tistory.com/564</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 4월3일 면접을 보게 되었습니다. 그때 나왔던 질문중 하나가 인덱스에 대한 질문이었습니다. 과거에 인덱스를 활용해서 성능을 개선해본적이 있었냐는 질문이었습니다. 했었던거 같아 일단 답변은 하긴했었는데 정재되지 않았습니다. 버벅된게 아니라 무슨 답변을 했는지도 기억이 안나는 정도였으니까요. 저는 인덱스를 통해 읽기 성능을 올렸던적이 있었다고 답변하였는데 지금 생각해보면 이것도 애매한 답변이었던거 같습니다. 왜냐하면 성능을 올렸던적이 있었다면 정확한 수치가 필요했을텐데 그렇지 못했으니까요.&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;/h2&gt;
&lt;figure id=&quot;og_1775478086333&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;기존 테스트..&quot; data-og-description=&quot;인덱스 적용브랜드 Id만 인덱스를 거는 경우는 어떨까? 생각보다 속도는 비슷하게 나오는듯하다.요렇게 나왔는데 이건 안 거는거랑 비슷한거 같다라는 생각이 들었다.그래서 좋아요를 기준으로&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/461&quot; data-og-url=&quot;https://b-programmer.tistory.com/461&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bD3KjS/dJMb8YpV5g1/49S1srdUmGyuwJhkxdkS51/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/tbxN3/dJMb8Xkf3ml/KNBYJytBRA0sw9OhetgoTK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/iiArd/dJMb8TCajq7/DktfvXjUexJqxoa9AH65B1/img.png?width=2092&amp;amp;height=272&amp;amp;face=0_0_2092_272&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/461&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/461&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bD3KjS/dJMb8YpV5g1/49S1srdUmGyuwJhkxdkS51/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/tbxN3/dJMb8Xkf3ml/KNBYJytBRA0sw9OhetgoTK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/iiArd/dJMb8TCajq7/DktfvXjUexJqxoa9AH65B1/img.png?width=2092&amp;amp;height=272&amp;amp;face=0_0_2092_272');&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;기존 테스트..&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;인덱스 적용브랜드 Id만 인덱스를 거는 경우는 어떨까? 생각보다 속도는 비슷하게 나오는듯하다.요렇게 나왔는데 이건 안 거는거랑 비슷한거 같다라는 생각이 들었다.그래서 좋아요를 기준으로&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스는 다들 알다시피 읽기 성능을 올릴 수 있습니다. 위 글을 확인해보면 2250 ms -&amp;gt; 590.58ms 약 73.75%가 감소했다는것을 확인할 수 있었습니다. 하지만 문제는 이것을 왜 했는지 그거에 대한 논리가 부족하다는점입니다. 아무래도 학습이 주가 되었기 때문에 읽기 속도가 늘었다는 점만 확인되었을 뿐이죠. 그렇다면, 어떻게 표현할 수 있을까요?&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;특정 조회 API에서 평균 2250ms의 지연이 발생하였고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;u&gt;실행 계획 분석 결과 Full Table Scan이 발생하고 있었습니다.&lt;/u&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;필터링과 정렬이 동시에 수행되는 쿼리 특성을 고려하여 인덱스를 설계하였고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;이를 통해 Index Scan 기반으로 실행 계획을 개선했습니다.&lt;br /&gt;그 결과 Latency를 2250ms &amp;rarr; 590.58ms로 약 73.75% 감소시킬 수 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면, 인덱스를 사용하면 어째서 Latency가 감소하나요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 사용하지 않는 경우, 조건에 맞는 데이터를 찾기 위해 테이블의 모든 데이터를 확인하게 됩니다.&lt;br /&gt;이는 테이블 전체를 순차적으로 읽는 Full Table Scan으로 이어집니다. &lt;br /&gt;반면 인덱스를 사용하면, 정렬된 구조를 기반으로 필요한 데이터의 위치를 빠르게 탐색할 수 있습니다. &lt;br /&gt;따라서 전체 데이터를 읽지 않고도 원하는 데이터에 접근할 수 있어, 결과적으로 읽기 성능이 향상되고 Latency가 감소합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다고 해서 인덱스를 건다고 해서 무조건 Latency가 감소하는 것은 아닙니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스는 조건에 따라 선택적으로 사용되기 때문에, 선택도가 낮거나 쿼리 패턴과 맞지 않는 경우에는 Full Table Scan이 더 효율적일 수 있습니다. 또한 인덱스가 많아질수록 쓰기 성능 저하와 추가적인 관리 비용이 발생할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤경우에 성능이 떨어지는지 자세하게 말씀해주세요.&lt;/h3&gt;
&lt;h4 data-end=&quot;292&quot; data-start=&quot;272&quot; data-section-id=&quot;fasavq&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;조회 결과가 너무 많은 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;304&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 전체 100만 건 중 70만 건을 읽어야 한다고 해봅시다.&lt;br /&gt;이때 인덱스를 타면 인덱스에서 조건에 맞는 위치를 찾고 다시 실제 데이터 페이지로 이동해서 읽어야 합니다&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;br /&gt;이 과정을 &lt;/span&gt;랜덤 I/O&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;가 많이 발생하는 구조라고 볼 수 있습니다. &lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;반면, 그냥 Full Table Scan은 테이블을 처음부터 끝까지 순서대로 읽습니다. &lt;/span&gt;데이터를 거의 다 봐야 한다면, 오히려 이런 순차 읽기가 더 효율적일 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;575&quot; data-start=&quot;560&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 경우입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1775483490816&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM users
WHERE gender = 'M';&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;701&quot; data-start=&quot;630&quot; data-ke-size=&quot;size16&quot;&gt;만약 남성 데이터가 전체의 50% 이상이라면, 인덱스를 타도 절반 이상을 다시 테이블에서 읽어야 하므로 큰 이점이 없습니다.&lt;br /&gt;&lt;u&gt;즉, 조건은 있지만, 실제로는 너무 많은 행을 가져오는 쿼리&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;에서는 &lt;/span&gt;인덱스가 큰 효과를 내지 못할 수 있습니다.&lt;/u&gt;&lt;/p&gt;
&lt;h4 data-end=&quot;808&quot; data-start=&quot;782&quot; data-section-id=&quot;1q9ap9h&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;선택도가 낮은 컬럼에 인덱스를 건 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;826&quot; data-start=&quot;810&quot; data-ke-size=&quot;size16&quot;&gt;이건 방금 사례와 연결됩니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;선택도는 간단히 말하면 &lt;/span&gt;&lt;b&gt;이 컬럼이 데이터를 얼마나 잘 구분해 주는가&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;입니다.&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;예를 들어 성별: 남/여, 상태값: Y/N, 삭제 여부: true/false이런 값은 종류가 적습니다.&lt;br /&gt;즉, 한 값으로 조회했을 때 너무 많은 행이 걸립니다.&lt;/p&gt;
&lt;p data-end=&quot;826&quot; data-start=&quot;810&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;span style=&quot;background-color: #dddddd;&quot;&gt; is_deleted = false&lt;/span&gt; 같은 조건은 대부분의 데이터가 false일 수 있습니다.&lt;br /&gt;그럼 인덱스를 타더라도 거의 전체 데이터를 읽게 됩니다.&lt;br /&gt;그래서 이런 컬럼 단독 인덱스는 기대만큼 성능이 안 나오는 경우가 많습니다.&lt;/p&gt;
&lt;h4 data-end=&quot;1158&quot; data-start=&quot;1122&quot; data-section-id=&quot;vum7kx&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;SELECT *처럼 인덱스만으로 처리가 안 되는 경우&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;1235&quot; data-start=&quot;1160&quot; data-ke-size=&quot;size16&quot;&gt;인덱스는 조건 검색에는 도움이 되지만, &lt;u&gt;실제 필요한 컬럼이 인덱스에 다 들어 있지 않으면 결국 테이블 본문까지 다시 읽어야 합니다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-end=&quot;1243&quot; data-start=&quot;1237&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1775483896912&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM product
WHERE brand_id = 10;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;brand_id에 인덱스가 있어도, &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;*&lt;/span&gt;를 조회하면 나머지 컬럼들을 가져오기 위해 테이블 페이지를 다시 읽어야 합니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, 인덱스는 탐색은 빠르게 해주지만 &lt;/span&gt;&lt;b&gt;최종적으로 읽어야 할 데이터가 많으면&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 비용이 커집니다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1447&quot; data-start=&quot;1432&quot; data-ke-size=&quot;size16&quot;&gt;반면 이런 경우는 좋습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1775483924078&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT brand_id, likes
FROM product
WHERE brand_id = 10
ORDER BY likes DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;br /&gt;만약 인덱스가 (brand_id, likes)로 잡혀 있고 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;조회 컬럼도 그 안에 다 있다면, 테이블을 다시 안 보고 인덱스만 읽어도 됩니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이걸 보통 &lt;/span&gt;&lt;b&gt;커버링 인덱스&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 관점에서 설명합니다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1659&quot; data-start=&quot;1654&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;인덱스를 썼더라도, 결국 테이블을 다시 많이 읽어야 하면 느려질 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; font-size: 1.25em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;복합 인덱스 순서가 쿼리와 맞지 않는 경우&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1773&quot; data-start=&quot;1743&quot; data-ke-size=&quot;size16&quot;&gt;복합 인덱스는 아무 순서로나 만들면 되는 게 아닙니다.&lt;/p&gt;
&lt;p data-end=&quot;1786&quot; data-start=&quot;1775&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 인덱스가 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;(brand_id, likes)&lt;/span&gt; &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이렇게 잡혀 있다고 해봅시다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그러면 보통은 brand_id를 먼저 활용한 뒤, 그 안에서 likes를 활용하는 구조가 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1920&quot; data-start=&quot;1896&quot; data-ke-size=&quot;size16&quot;&gt;이때 다음 쿼리는 잘 탈 가능성이 높습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1775484170625&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE brand_id = ?
ORDER BY likes DESC&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1995&quot; data-start=&quot;1973&quot; data-ke-size=&quot;size16&quot;&gt;그런데 이런 쿼리는 애매할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1775484183407&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE likes &amp;gt; 100&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2069&quot; data-start=&quot;2027&quot; data-ke-size=&quot;size16&quot;&gt;왜냐하면 인덱스의 첫 컬럼인 brand_id를 안 쓰고 있기 때문입니다.&lt;br /&gt;즉, 복합 인덱스는 앞쪽 컬럼부터 맞춰서 써야 효과가 큽니다.&lt;/p&gt;
&lt;h4 data-end=&quot;2212&quot; data-start=&quot;2184&quot; data-section-id=&quot;7jllr6&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;함수나 연산 때문에 인덱스를 못 타는 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;2255&quot; data-start=&quot;2214&quot; data-ke-size=&quot;size16&quot;&gt;컬럼에 가공을 해버리면 DB가 기존 인덱스를 그대로 활용하기 어려워집니다.&lt;/p&gt;
&lt;p data-end=&quot;2263&quot; data-start=&quot;2257&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1775484262226&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE YEAR(created_at) = 2026&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2378&quot; data-start=&quot;2307&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 쓰면 created_at 인덱스가 있어도 컬럼에 함수가 먼저 적용되기 때문에 인덱스를 제대로 못 탈 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;2400&quot; data-start=&quot;2380&quot; data-ke-size=&quot;size16&quot;&gt;보통은 이렇게 바꾸는 편이 좋습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1775484278062&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE created_at &amp;gt;= '2026-01-01'
AND created_at &amp;lt; '2027-01-01'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;또 비슷하게&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1775484288576&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE price + 10 &amp;gt; 100&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2557&quot; data-start=&quot;2523&quot; data-ke-size=&quot;size16&quot;&gt;이런 식으로 컬럼에 계산이 들어가도 인덱스 활용이 어렵습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;핵심은 &lt;/span&gt;인덱스는 원본 값 기준으로 정렬되어 있는데, 쿼리에서 값을 바꿔버리면 그 정렬 구조를 활용하기 어려워진다&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;는 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-end=&quot;2672&quot; data-start=&quot;2647&quot; data-section-id=&quot;8qshyv&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;앞에 와일드카드가 붙는 LIKE 검색&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;2686&quot; data-start=&quot;2674&quot; data-ke-size=&quot;size16&quot;&gt;이것도 매우 흔합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1775484335905&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE name LIKE '%phone'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2788&quot; data-start=&quot;2725&quot; data-ke-size=&quot;size16&quot;&gt;이 경우는 문자열 앞부분이 고정되어 있지 않기 때문에 B-Tree 인덱스로 어디서부터 찾아야 할지 애매합니다.&lt;br /&gt;반면 이런 경우는 비교적 유리합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1775484348691&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE name LIKE 'phone%'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2880&quot; data-start=&quot;2849&quot; data-ke-size=&quot;size16&quot;&gt;앞부분이 정해져 있으면 인덱스 범위 탐색이 가능해집니다.&lt;/p&gt;
&lt;p data-end=&quot;2884&quot; data-start=&quot;2882&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2959&quot; data-start=&quot;2886&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2912&quot; data-start=&quot;2886&quot; data-section-id=&quot;2b3mfh&quot;&gt;'abc%' &amp;rarr; 인덱스 활용 가능성 높음&lt;/li&gt;
&lt;li data-end=&quot;2936&quot; data-start=&quot;2913&quot; data-section-id=&quot;pmn993&quot;&gt;'%abc' &amp;rarr; 인덱스 활용 어려움&lt;/li&gt;
&lt;li data-end=&quot;2959&quot; data-start=&quot;2937&quot; data-section-id=&quot;vzl50f&quot;&gt;'%abc%' &amp;rarr; 보통 더 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3023&quot; data-start=&quot;2961&quot; data-ke-size=&quot;size16&quot;&gt;그래서 contains 검색이 많으면 일반 인덱스보다 풀텍스트 검색이나 별도 검색 엔진을 고민하기도 합니다.&lt;/p&gt;
&lt;h4 data-end=&quot;3055&quot; data-start=&quot;3030&quot; data-section-id=&quot;1yc55he&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;정렬과 인덱스 방향이 맞지 않는 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;3131&quot; data-start=&quot;3057&quot; data-ke-size=&quot;size16&quot;&gt;인덱스는 검색뿐 아니라 정렬에도 영향을 줍니다. 그런데 정렬 조건이 인덱스 구조와 안 맞으면 결국 별도 정렬 작업이 필요해집니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 필터는 brand_id로 하고, 정렬은 created_at인데 &lt;br /&gt;&lt;/span&gt;인덱스가 (brand_id, likes)라면 created_at 정렬에는 직접 도움이 안 됩니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그러면 DB는 &lt;/span&gt;조건으로 일부 찾고 결과를 따로 정렬하게 됩니다.&lt;br /&gt;이 정렬 비용이 큰 경우 Latency가 늘어납니다.&lt;/p&gt;
&lt;p data-end=&quot;3320&quot; data-start=&quot;3318&quot; data-ke-size=&quot;size16&quot;&gt;즉, 필터링과 정렬을 함께 고려하지 않은 인덱스&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;는 기대보다 효과가 약할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-end=&quot;3397&quot; data-start=&quot;3379&quot; data-section-id=&quot;ewjedu&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;쓰기 작업이 많은 테이블&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;3465&quot; data-start=&quot;3399&quot; data-ke-size=&quot;size16&quot;&gt;인덱스는 조회를 빠르게 해주지만, INSERT, UPDATE, DELETE 시에는 오히려 부담이 됩니다.&lt;br /&gt;왜냐하면 테이블 데이터만 바꾸는 게 아니라 관련 인덱스도 같이 수정해야 하기 때문입니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 인덱스가 1개면 한 번 수정할 걸, &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;인덱스가 5개면 5군데를 더 관리해야 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3618&quot; data-start=&quot;3585&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3589&quot; data-start=&quot;3585&quot; data-section-id=&quot;yiksfg&quot;&gt;주문&lt;/li&gt;
&lt;li data-end=&quot;3594&quot; data-start=&quot;3590&quot; data-section-id=&quot;yi3n5w&quot;&gt;결제&lt;/li&gt;
&lt;li data-end=&quot;3604&quot; data-start=&quot;3595&quot; data-section-id=&quot;1e0959x&quot;&gt;로그성 이벤트&lt;/li&gt;
&lt;li data-end=&quot;3618&quot; data-start=&quot;3605&quot; data-section-id=&quot;yg1ulh&quot;&gt;실시간 트래픽 테이블&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3671&quot; data-start=&quot;3620&quot; data-ke-size=&quot;size16&quot;&gt;처럼 쓰기 비중이 높은 곳에서는 인덱스를 많이 걸수록 전체 처리량이 떨어질 수 있습니다.&lt;br /&gt;즉, 조회 한 번 빠르게 하려다가, 전체 쓰기 성능이 계속 손해 볼 수 있습니다.&lt;/p&gt;
&lt;h4 data-end=&quot;3778&quot; data-start=&quot;3751&quot; data-section-id=&quot;3wtc2u&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;업데이트가 잦은 컬럼에 인덱스를 건 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;3872&quot; data-start=&quot;3780&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 view_count, likes, updated_at처럼 값이 자주 바뀌는 컬럼에 인덱스를 걸면 인덱스 재정렬/유지 비용이 자주 발생합니다.&lt;br /&gt;이 경우는 단순 조회뿐 아니라 수정이 반복될 때마다 인덱스도 계속 손봐야 하므로 부담이 커질 수 있습니다.&lt;br /&gt;그래서 자주 바뀌는 컬럼은 정말 필요한 경우가 아니면 신중하게 봐야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;4595&quot; data-start=&quot;4540&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;4625&quot; data-start=&quot;4602&quot; data-section-id=&quot;1f5mhng&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;조인에서 인덱스가 어긋나는 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;4643&quot; data-start=&quot;4627&quot; data-ke-size=&quot;size16&quot;&gt;조인도 인덱스 영향이 큽니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 두 테이블을 조인할 때 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;조인 컬럼에 인덱스가 없으면, 한쪽 데이터를 반복적으로 훑으면서 비용이 커질 수 있습니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;특히 큰 테이블끼리 조인할 때 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;조인 키 인덱스가 없으면 성능이 급격히 나빠질 수 있습니다.&lt;br /&gt;&lt;/span&gt;반대로 인덱스를 걸었더라도 조인 순서나 필터 조건이 맞지 않으면 기대만큼 개선이 안 될 수도 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-end=&quot;4643&quot; data-start=&quot;4627&quot; data-ke-size=&quot;size16&quot;&gt;결국, 인덱스는 항상 Latency를 줄이는 것이 아닙니다.&lt;br /&gt;조회 대상이 너무 많거나, 선택도가 낮은 컬럼에 인덱스를 건 경우에는 효과가 작을 수 있습니다.&lt;br /&gt;또한 함수 사용, 와일드카드 LIKE, 복합 인덱스 순서 불일치처럼 쿼리 패턴이 인덱스 구조와 맞지 않으면 인덱스를 제대로 활용하지 못합니다. 반대로 인덱스가 많아지면 INSERT/UPDATE/DELETE 시 유지 비용이 증가해 전체 성능이 저하될 수도 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;4643&quot; data-start=&quot;4627&quot; data-ke-size=&quot;size16&quot;&gt;이제 정리할것은 2가지정도 남은것으로 확인되었습니다.&lt;/p&gt;
&lt;p data-end=&quot;4643&quot; data-start=&quot;4627&quot; data-ke-size=&quot;size16&quot;&gt;1. 커버링 인덱스&amp;nbsp;&lt;br /&gt;2. 함수에는 인덱스가 먹히지 않는 이유&amp;nbsp;&lt;br /&gt;이 두개는 간단하게 작성하고 넘어가겠습니다.~&lt;/p&gt;
&lt;p data-end=&quot;4643&quot; data-start=&quot;4627&quot; data-ke-size=&quot;size16&quot;&gt;커버링 인덱스는 쿼리에 필요한 모든 컬럼을 인덱스만으로 해결하는 것을 뜻합니다.&lt;br /&gt;함수에 인덱스가 먹히지 않는 이유는 인덱스는 원본 값 기준 정렬인데, 함수는 값을 바꿔버리기 때문에 그 정렬을 못 쓴다&lt;/p&gt;
&lt;h2 data-end=&quot;4643&quot; data-start=&quot;4627&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 사용하면 어떻게 성능이 오르는지 언제 사용하는지에 대해 알아봤습니다. 만약에, 인덱스 관련 질문이 들어온다면, 일단은 다음과 같이 답변을 할것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;특정 조회 API에서 평균 2250ms의 지연이 발생하였고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;실행 계획 분석 결과 Full Table Scan이 발생하고 있었습니다.&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;필터링과 정렬이 동시에 수행되는 쿼리 특성을 고려하여 인덱스를 설계하였고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;이를 통해 Index Scan 기반으로 실행 계획을 개선했습니다.&lt;br /&gt;그 결과 Latency를 2250ms &amp;rarr; 590.58ms로 약 73.75% 감소시킬 수 있었습니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/564</guid>
      <comments>https://b-programmer.tistory.com/564#entry564comment</comments>
      <pubDate>Mon, 6 Apr 2026 23:30:22 +0900</pubDate>
    </item>
    <item>
      <title>배치는 언제 사용이 되어질까?</title>
      <link>https://b-programmer.tistory.com/563</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 레디스를 언제 쓰고, 카프카를 언제 써야 하는지에 대해서는 어느 정도 감이 잡힌 것 같다. 그런데 이상하게도 이 두 기술보다 더 직관적으로 느껴질 것 같은 스프링 배치는 여전히 잘 와닿지 않는다. 분명 사용해본 적은 있지만, 전체 흐름이 머릿속에 자연스럽게 그려지지는 않는다. 아마 구조가 복잡해서일 수도 있지만, 그보다 더 큰 이유는 내가 배치의 핵심을 제대로 이해하지 못했기 때문일지도 모른다. 그래서 이번 기회에 스프링 배치의 본질을 제대로 짚고 넘어가보려고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 본질이 뭔데?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;google은 배치를 다음과 같이 설명하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOLnJd/dJMcajuX3EQ/oKpYl7F6vXp4LYVH8ZyQg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOLnJd/dJMcajuX3EQ/oKpYl7F6vXp4LYVH8ZyQg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOLnJd/dJMcajuX3EQ/oKpYl7F6vXp4LYVH8ZyQg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOLnJd%2FdJMcajuX3EQ%2FoKpYl7F6vXp4LYVH8ZyQg1%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;1334&quot; height=&quot;230&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;230&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;1130&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JQCKi/dJMcaaSnoN2/RJGty6i0ZgAEADTlwg4OQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JQCKi/dJMcaaSnoN2/RJGty6i0ZgAEADTlwg4OQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JQCKi/dJMcaaSnoN2/RJGty6i0ZgAEADTlwg4OQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJQCKi%2FdJMcaaSnoN2%2FRJGty6i0ZgAEADTlwg4OQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;644&quot; height=&quot;304&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치는 데이터를 한 번에 모아서 처리하는 방식으로 이해할 수 있다. &lt;br /&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;u&gt;데이터베이스, 파일, 외부 API, 메시지 큐 등 다양한 곳&lt;/u&gt;에서 유입될 수 있다.&lt;br /&gt;하지만 배치에서 더 중요한 것은 데이터가 어디서 왔는지가 아니라, 쌓인 데이터를 어떤 방식으로 안정적으로 처리할 것인가이다.&lt;br /&gt;즉, 배치의 본질은 데이터의 입력 경로가 아니라 대량의 데이터를 어떻게 읽고, 가공하고, 끝까지 정확하게 처리할 것인가에 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면, 어떻게 데이터를 처리할 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치는 복잡해 보이지만, 본질적인 처리 방식은 의외로 단순하다.&lt;br /&gt;바로 &lt;b&gt;읽고, 가공하고, 저장하는 것&lt;/b&gt;이다.&lt;br /&gt;즉, 배치의 기본 흐름은 &lt;b&gt;읽기 &amp;rarr; 가공 &amp;rarr; 저장&lt;/b&gt; 으로 정리할 수 있다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그런데 가만히 생각해보면, 이런 과정은 반복문만으로도 충분히 구현할 수 있어 보인다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775307813962&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;?&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
for (data : list) {
process(data);
}&lt;/code&gt;&lt;/pre&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;60bc76e0-2d2f-4508-b461-bc01adbba41f&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-end=&quot;403&quot; data-start=&quot;323&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 읽고, 가공하고, 저장한다. 이 자체만 놓고 보면 분명 원하는 처리는 가능하다.&lt;br /&gt;하지만 이것은 어디까지나 동작에 가깝다.&lt;/p&gt;
&lt;p data-end=&quot;525&quot; data-start=&quot;405&quot; data-ke-size=&quot;size16&quot;&gt;반면 스프링 배치는 단순히 동작만 수행하는 데서 끝나지 않는다.&lt;br /&gt;어디까지 처리했는지 기록하고, 중간에 실패하면 다시 이어서 실행할 수 있으며, 처리 결과를 남기고, &lt;br /&gt;운영자가 전체 흐름을 추적할 수 있도록 돕는다.&lt;br /&gt;즉, 스프링 배치의 핵심은 단순한 반복 처리가 아니라, 동작을 운영 가능한 시스템으로 만드는 것에 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략적으로 배치에 대해 학습을 진행하였습니다. 배치를 어떻게 사용하면 좋을까 또, 왜 사용하는게 좋을지에 대해 고민을 했던거 같습니다.&lt;br /&gt;짧게 쓴글이지만, 추후에 어떻게 읽고 하고 어떤것을 사용하고 처리는 어떻게 하는지 심도 깊게 학습해볼 예정입니다.&amp;nbsp;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/563</guid>
      <comments>https://b-programmer.tistory.com/563#entry563comment</comments>
      <pubDate>Sat, 4 Apr 2026 22:10:52 +0900</pubDate>
    </item>
    <item>
      <title>LLM이란 무엇일까?</title>
      <link>https://b-programmer.tistory.com/562</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 &lt;span&gt;&lt;span&gt;ChatGPT&lt;/span&gt;&lt;/span&gt;, &lt;span&gt;&lt;span&gt;Gemini&lt;/span&gt;&lt;/span&gt;, &lt;span&gt;&lt;span&gt;Grok&lt;/span&gt;&lt;/span&gt; 등 다양한 LLM 서비스가 빠르게 확산되고 있습니다. LLM(Large Language Model)은 말 그대로 대규모 언어 모델을 의미하며, 인간의 언어를 이해하고 생성할 수 있는 인공지능 기술입니다. 불과 몇 년 전까지만 해도 LLM은 일부 기업이나 연구 기관만 다룰 수 있는 고난도 기술이었습니다. 하지만 현재는 상황이 완전히 달라졌습니다. 누구나 손쉽게 사용할 수 있고, 나아가 직접 활용하거나 구축까지 가능한 수준으로 대중화되었습니다. 지금은 마치 전기나 클라우드처럼 LLM을 '구독'하여 사용하는 시대입니다. 그러나 머지않아 개인이 LLM을 기반으로 서비스를 만들고, 이를 통해 직접 가치를 창출하는 시대가 올지도 모릅니다. 이러한 변화 속에서 LLM을 단순히 사용하는 것을 넘어, 그 구조와 동작 방식을 이해하는 것은 매우 중요하다고 판단했습니다. 따라서 본 글에서는 LLM이 무엇인지에 대해 기초부터 정리하며 학습해보고자 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LLM이 등장하기 이전&lt;/h2&gt;
&lt;p data-end=&quot;899&quot; data-start=&quot;808&quot; data-ke-size=&quot;size16&quot;&gt;LLM이 등장하기 이전에는 자연어 처리를 위해 사람이 직접 정의한 규칙 기반 방식이나, 제한적인 패턴 인식 기술이 주로 사용되었습니다.&lt;br /&gt;이러한 방식은 특정 패턴을 처리하는 데에는 효과적이었지만, 문맥을 이해하거나 다양한 표현을 유연하게 처리하는 데에는 한계가 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;453&quot; data-start=&quot;358&quot; data-ke-size=&quot;size16&quot;&gt;이후 이러한 한계를 보완하기 위해 통계 기반 모델이 등장하였습니다.&lt;br /&gt;대표적으로 n-gram 모델은 &lt;u&gt;이전 단어들의 등장 확률을 기반으로 다음 단어를 예측하는 방식&lt;/u&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;556&quot; data-start=&quot;455&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, &quot;나는 오늘 밥을&quot;이라는 문장이 주어지면, 학습 데이터에서 &quot;밥을 먹다&quot;라는 표현이 자주 등장했기 때문에&lt;br /&gt;&quot;먹었다&quot;가 나올 확률이 높다고 판단하는 방식입니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하지만 n-gram 모델은 &lt;u&gt;앞의 일부 단어만 참고할 수 있다는 한계가 있었고, &lt;/u&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;u&gt;문장이 길어질수록 전체 의미를 제대로 반영하기 어려웠습니다.&lt;/u&gt;&lt;br /&gt;&lt;/span&gt;이러한 문제를 해결하기 위해 등장한 것이 RNN(Recurrent Neural Network)입니다.&lt;br /&gt;RNN은 &lt;u&gt;이전 정보를 순차적으로 전달하면서 문맥을 반영하려는 구조&lt;/u&gt;를 가지고 있습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그러나 RNN 역시 다음과 같은 한계를 가지고 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;868&quot; data-start=&quot;781&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;810&quot; data-start=&quot;781&quot; data-section-id=&quot;xlvvsk&quot;&gt;문장이 길어질수록 앞의 정보를 잃어버리는 문제&lt;/li&gt;
&lt;li data-end=&quot;832&quot; data-start=&quot;811&quot; data-section-id=&quot;1vlt5es&quot;&gt;학습이 어렵고 속도가 느린 구조&lt;/li&gt;
&lt;li data-end=&quot;868&quot; data-start=&quot;833&quot; data-section-id=&quot;1efea2r&quot;&gt;장기 의존성(Long-term dependency) 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;962&quot; data-start=&quot;870&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 자연어 처리 기술은 점진적으로 발전해왔지만, 여전히 문장을 전체적으로 이해하기보다는&lt;br /&gt;부분적인 정보나 패턴을 기반으로 처리하는 수준에 머물러 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;1022&quot; data-start=&quot;964&quot; data-ke-size=&quot;size16&quot;&gt;이러한 한계를 극복하기 위해 등장한 것이 바로 Transformer 기반의 LLM입니다.&lt;/p&gt;
&lt;h2 data-end=&quot;1022&quot; data-start=&quot;964&quot; data-ke-size=&quot;size26&quot;&gt;Transformer은 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transformer는 &lt;u&gt;자연어 처리에서 사용되는 딥러닝 모델 구조로, 문장을 순차적으로 처리하지 않고 전체를 한 번에 이해하도록 설계된 모델&lt;/u&gt;입니다. 기존의 RNN은 단어를 순서대로 처리하는 구조를 가지고 있었기 때문에 문장이 길어질수록 &lt;br /&gt;&lt;u&gt;앞의 정보를 점점 잃어버리는 한계&lt;/u&gt;가 있었습니다. &lt;br /&gt;이러한 문제를 해결하기 위해 등장한 것이 Transformer이며, 핵심은 Self-Attention 메커니즘에 있습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;599&quot; data-start=&quot;575&quot; data-section-id=&quot;1km68d7&quot; data-ke-size=&quot;size23&quot;&gt;Self-Attention이란?&lt;/h3&gt;
&lt;p data-end=&quot;674&quot; data-start=&quot;601&quot; data-ke-size=&quot;size16&quot;&gt;Self-Attention은 &lt;u&gt;문장 안의 단어들이 서로를 참고하여 각 단어 간의 관계와 중요도를 계산하는 방식&lt;/u&gt;입니다.&lt;br /&gt;예를 들어, &quot;나는 어제 도서관에서 책을 읽었다&quot;라는 문장에서 &quot;읽었다&quot;라는 단어는&lt;br /&gt;&quot;책&quot;, &quot;도서관&quot;과의 관계를 함께 고려하여 의미가 결정됩니다.&lt;br /&gt;즉, Transformer는 단어를 순서대로 이해하는 것이 아니라 문장 전체의 관계 속에서 의미를 파악합니다.&lt;/p&gt;
&lt;h2 data-end=&quot;674&quot; data-start=&quot;601&quot; data-ke-size=&quot;size26&quot;&gt;그래서 LLM이 뭔데..??&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM(Large Language Model)은 &lt;u&gt;사람의 언어를 이해하고, 생성하며, 자연스럽게 응답하도록 설계된 신경망 모델&lt;/u&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;331&quot; data-start=&quot;232&quot; data-ke-size=&quot;size16&quot;&gt;여기서 &quot;대규모(Large)&quot;는 단순히 모델의 크기만을 의미하는 것이 아니라, 수십억 개 이상의 파라미터와 방대한 텍스트 데이터로 학습되었다는 의미를 포함합니다. LLM은 입력된 문장을 기반으로 다음에 올 단어를 확률적으로 예측하는 방식으로 동작하며,&lt;br /&gt;이 과정에서 문장 전체의 맥락을 반영하기 위해 Transformer 구조를 사용합니다.&lt;/p&gt;
&lt;p data-end=&quot;538&quot; data-start=&quot;439&quot; data-ke-size=&quot;size16&quot;&gt;특히 Transformer의 핵심인 Self-Attention 메커니즘을 통해 입력 문장의 각 요소들 간의 관계를 고려하며 중요한 정보에 선택적으로 집중할 수 있습니다. 현재 AI 분야에서는 머신러닝과 딥러닝 기반 접근 방식이 주를 이루고 있지만,&lt;br /&gt;과거에는 규칙 기반 시스템, 전문가 시스템, 퍼지 논리, 기호 추론과 같은 다양한 방식들도 함께 연구되어 왔습니다.&lt;br /&gt;LLM은 이러한 전통적인 접근 방식과 달리, 데이터를 기반으로 패턴을 학습하고 일반화할 수 있는 모델이라는 점에서 큰 차이를 가집니다.&lt;/p&gt;
&lt;h2 data-end=&quot;538&quot; data-start=&quot;439&quot; data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;513&quot; data-start=&quot;48&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;196&quot; data-start=&quot;48&quot; data-section-id=&quot;1597qcj&quot;&gt;LLM은 최근 &lt;span&gt;&lt;span&gt;ChatGPT&lt;/span&gt;&lt;/span&gt;, &lt;span&gt;&lt;span&gt;Gemini&lt;/span&gt;&lt;/span&gt;, &lt;span&gt;&lt;span&gt;Grok&lt;/span&gt;&lt;/span&gt; 등을 통해 빠르게 대중화된 기술이다.&lt;/li&gt;
&lt;li data-end=&quot;270&quot; data-start=&quot;197&quot; data-section-id=&quot;1tj82nu&quot;&gt;과거에는 규칙 기반, 패턴 인식, n-gram, RNN등과 같은 방식으로 자연어를 처리했지만 문맥 이해에 한계가 있었다.&lt;/li&gt;
&lt;li data-end=&quot;366&quot; data-start=&quot;271&quot; data-section-id=&quot;1t4dkz6&quot;&gt;이러한 한계를 해결하기 위해 Transformer 구조가 등장했고, Self-Attention을 통해 문장 전체의 관계를 이해할 수 있게 되었다.&lt;/li&gt;
&lt;li data-end=&quot;448&quot; data-start=&quot;367&quot; data-section-id=&quot;yr5ct0&quot;&gt;LLM은 이 Transformer를 기반으로 대규모 데이터와 파라미터를 활용해 다음 단어를 확률적으로 예측하는 모델이다.&lt;/li&gt;
&lt;li data-end=&quot;513&quot; data-start=&quot;449&quot; data-section-id=&quot;fwjmrn&quot;&gt;기존 방식과 달리 데이터 기반으로 패턴을 학습하고 일반화할 수 있다는 점이 핵심 차이이다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/LLM</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/562</guid>
      <comments>https://b-programmer.tistory.com/562#entry562comment</comments>
      <pubDate>Wed, 1 Apr 2026 17:35:34 +0900</pubDate>
    </item>
    <item>
      <title>로드벨런서 vs API 게이트웨이</title>
      <link>https://b-programmer.tistory.com/561</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하다 보면 헷갈리는 용어를 자주 마주하게 됩니다. 대표적인 예가 바로 로드 밸런서와 API 게이트웨이입니다. 이름만 보면 서로 전혀 다른 역할을 하는 것처럼 느껴집니다. 하지만 실제로 어떤 일을 하는지 떠올려 보면, 둘 다 &quot;요청을 전달한다&quot;는 공통점 때문에 비슷한 개념처럼 느껴지기도 합니다. 이러한 혼란은 개념을 충분히 분리해서 이해하지 못했을 때 자연스럽게 발생합니다. 이번 글에서는 로드 밸런서와 API 게이트웨이의 차이점과 공통점을 명확하게 정리해보겠습니다.&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;br /&gt;이 과정에서 &quot;&lt;u&gt;중간에서 요청을 처리한다&lt;/u&gt;&quot;는 점 때문에 두 개념이 비슷하게 느껴질 수 있습니다.&lt;br /&gt;예를 들어, 클라이언트가 요청을 보내면 로드 밸런서든 API 게이트웨이든 직접 요청을 처리하는 것이 아니라, 다른 서버로 전달합니다.&lt;br /&gt;즉, 두 기술 모두 시스템의 앞단에서 동작하며, 요청을 적절한 위치로 보내는 역할을 수행합니다.&lt;br /&gt;또한, 두 기술 모두 트래픽을 분산시키는 것처럼 보일 수 있습니다. &lt;br /&gt;로드 밸런서는 실제로 여러 서버로 요청을 분산시키고, API 게이트웨이 역시 여러 서비스로 요청을 라우팅하기 때문입니다.&lt;br /&gt;아래 그림을 보면 HTTPS 요청이 로드 밸런서를 통해 여러 서버로 분산되어 전달되는 것을 확인할 수 있습니다.&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;1531&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QTXSg/dJMcai3OdZ0/GUNNtRrQfGU3uFxzvESffK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QTXSg/dJMcai3OdZ0/GUNNtRrQfGU3uFxzvESffK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QTXSg/dJMcai3OdZ0/GUNNtRrQfGU3uFxzvESffK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQTXSg%2FdJMcai3OdZ0%2FGUNNtRrQfGU3uFxzvESffK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;459&quot; height=&quot;549&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1531&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림을 커다란 이미지로 다시 그려보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNPJfi/dJMcaaSj8DF/koGHV3k5BOxDOfMf4atqk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNPJfi/dJMcaaSj8DF/koGHV3k5BOxDOfMf4atqk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNPJfi/dJMcaaSj8DF/koGHV3k5BOxDOfMf4atqk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNPJfi%2FdJMcaaSj8DF%2FkoGHV3k5BOxDOfMf4atqk0%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;202&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;582&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;h2 data-ke-size=&quot;size26&quot;&gt;로드 벨런서&lt;/h2&gt;
&lt;p data-end=&quot;397&quot; data-start=&quot;276&quot; data-ke-size=&quot;size16&quot;&gt;로드 밸런서는 클라이언트로부터 들어오는 요청을 여러 서버로 분산시켜, 특정 서버에 트래픽이 몰리지 않도록 하는 역할을 수행합니다.&lt;/p&gt;
&lt;p data-end=&quot;350&quot; data-start=&quot;215&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 증가하거나 트래픽이 급증하는 상황에서는 하나의 서버만으로 모든 요청을 처리하기 어렵습니다.&lt;br /&gt;이때 로드 밸런서를 앞단에 두면 요청을 여러 서버로 나누어 처리할 수 있기 때문에, 전체 시스템의 안정성과 처리량을 향상시킬 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;373&quot; data-start=&quot;352&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 로드 밸런서가 왜 필요할까요?&lt;/p&gt;
&lt;p data-end=&quot;469&quot; data-start=&quot;375&quot; data-ke-size=&quot;size16&quot;&gt;서버를 여러 대로 확장하더라도, 클라이언트는 어떤 서버로 요청을 보내야 할지 알 수 없습니다.&lt;br /&gt;또한 사용자 입장에서 모든 서버의 주소를 기억하는 것도 비효율적입니다.&lt;/p&gt;
&lt;p data-end=&quot;541&quot; data-start=&quot;471&quot; data-ke-size=&quot;size16&quot;&gt;이러한 상황에서는 결국 특정 서버로만 요청이 집중될 가능성이 높아지고, 서버를 여러 대로 운영하는 의미가 사라지게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;642&quot; data-start=&quot;543&quot; data-ke-size=&quot;size16&quot;&gt;로드 밸런서는 이러한 문제를 해결하기 위해 존재합니다. 클라이언트는 하나의 진입점만 알고 있으면 되고, 로드 밸런서는 내부적으로 여러 서버에 요청을 균등하게 분산시켜 줍니다. 대표적인 예로는 &lt;span&gt;&lt;span&gt;Nginx&lt;/span&gt;&lt;/span&gt;가 있습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;397&quot; data-start=&quot;276&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;API 게이트 웨이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, API 게이트 웨이는 어떨까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 게이트웨이는 클라이언트의 요청을 받아 적절한 서비스로 전달하는 역할을 수행합니다.&lt;br /&gt;하지만 단순히 요청을 전달하는 것에 그치지 않고, 요청을 처리하는 과정에서 다양한 로직을 함께 수행합니다.&lt;/p&gt;
&lt;p data-end=&quot;336&quot; data-start=&quot;215&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 클라이언트의 요청이 들어오면 API 게이트웨이는 먼저 인증 및 인가를 확인하고, 요청의 유효성을 검증합니다.&lt;br /&gt;이후 요청을 적절한 서비스로 라우팅하며, 필요에 따라 요청이나 응답을 변환하기도 합니다.&lt;/p&gt;
&lt;p data-end=&quot;362&quot; data-start=&quot;338&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 API 게이트웨이는 왜 필요할까요?&lt;/p&gt;
&lt;p data-end=&quot;499&quot; data-start=&quot;364&quot; data-ke-size=&quot;size16&quot;&gt;마이크로서비스 아키텍처에서는 여러 개의 서비스가 존재하게 됩니다. 이때 클라이언트가 각각의 서비스에 직접 요청을 보내게 되면,&lt;br /&gt;서비스의 주소를 모두 알아야 하고, 인증이나 로깅과 같은 공통 기능도 각각 처리해야 하는 문제가 발생합니다.&lt;/p&gt;
&lt;p data-end=&quot;645&quot; data-start=&quot;501&quot; data-ke-size=&quot;size16&quot;&gt;API 게이트웨이는 이러한 문제를 해결하기 위해 존재합니다. 클라이언트는 하나의 진입점만 호출하면 되고, API 게이트웨이가 내부적으로 적절한 서비스로 요청을 전달합니다. 또한 인증, 로깅, 유량 제어와 같은 공통 기능을 중앙에서 관리할 수 있습니다.&lt;br /&gt;대표적으로는 &lt;span&gt;&lt;span&gt;Spring Cloud Gateway&lt;/span&gt;&lt;/span&gt;와 &lt;span&gt;&lt;span&gt;Kong&lt;/span&gt;&lt;/span&gt; 등이 있습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;645&quot; data-start=&quot;501&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 로드 밸런서와 API 게이트웨이의 공통점과 차이점에 대해 살펴보았습니다. 제가 생각하는 두 기술의 가장 큰 차이는 분산과 관리입니다. 로드 밸런서는 여러 서버를 하나의 서버처럼 사용할 수 있도록, 앞단에서 트래픽을 통합하고 이를 각 서버에 균등하게 분산시키는 역할을 합니다. 반면, API 게이트웨이는 주로 MSA 환경에서 사용되며, 여러 서비스(도메인)를 하나의 진입점으로 통합하고 인증, 라우팅, 로깅과 같은 기능을 통해 이를 관리하는 역할을 수행합니다. 결국 두 기술은 모두 요청을 전달하는 중간 계층이라는 공통점을 가지지만, 로드 밸런서는 트래픽 분산, API 게이트웨이는 요청 관리와 제어에 초점을 둔다는 차이가 있습니다. 멀리서 보면 비슷해 보이지만, 역할과 책임을 기준으로 보면 명확하게 구분되는 기술이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;br /&gt;&lt;a href=&quot;https://blog.bytebytego.com/p/ep208-load-balancer-vs-api-gateway&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.bytebytego.com/p/ep208-load-balancer-vs-api-gateway&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/561</guid>
      <comments>https://b-programmer.tistory.com/561#entry561comment</comments>
      <pubDate>Tue, 31 Mar 2026 22:26:07 +0900</pubDate>
    </item>
    <item>
      <title>리플렉션 deep 하게 JDK관점에서 풀어보기</title>
      <link>https://b-programmer.tistory.com/560</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 여러 객체를 프록시로 생성하거나, 프록시 객체를 활용하는 과정에서 Java는 자연스럽게 리플렉션(Reflection)이라는 기술을 사용하게 됩니다.&lt;/p&gt;
&lt;figure id=&quot;og_1774867357756&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;문제 발견: LazyConnectionDataSourceProxy 톺아보기&quot; data-og-description=&quot;read-only일때 replica 사용하게 하기master-slave 구조 이해하기DB에서 master-slave 형태로 변경하다는 뜻은 읽기 전용과 쓰기 전용을 분리한다는 의미입니다.최근에는 master-slave라는 용어보다는 primary-repl&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/550&quot; data-og-url=&quot;https://b-programmer.tistory.com/550&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qAQSx/dJMb8YXLzEQ/c7GtO8Mkq62HSqR1g9DGX0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bfHCxS/dJMb8Rj1Zmp/B2HjltZSdGd15I6K92IKe1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bpAgbl/dJMb8QemgGW/nsoVKIzEXKvkSm37GXKATK/img.png?width=2048&amp;amp;height=402&amp;amp;face=0_0_2048_402&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/550&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/550&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qAQSx/dJMb8YXLzEQ/c7GtO8Mkq62HSqR1g9DGX0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bfHCxS/dJMb8Rj1Zmp/B2HjltZSdGd15I6K92IKe1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bpAgbl/dJMb8QemgGW/nsoVKIzEXKvkSm37GXKATK/img.png?width=2048&amp;amp;height=402&amp;amp;face=0_0_2048_402');&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;문제 발견: LazyConnectionDataSourceProxy 톺아보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;read-only일때 replica 사용하게 하기master-slave 구조 이해하기DB에서 master-slave 형태로 변경하다는 뜻은 읽기 전용과 쓰기 전용을 분리한다는 의미입니다.최근에는 master-slave라는 용어보다는 primary-repl&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1774867373940&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;분명... slow쿼리를 모니터링하려고 했는데... 어째서...&quot; data-og-description=&quot;return wrapPreparedStatement(preparedStatement, sql);​ 슬로쿼리 모니터링 인터페이스 제작슬로 쿼리를 모니터링 인터페이스를 만들기위해 어떤 작업을 해야 할까요? slow 쿼리는 어떻게 탐지할 수 있을까?Sl&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/544&quot; data-og-url=&quot;https://b-programmer.tistory.com/544&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bN8cDM/dJMb83ksZng/GnxiZit8oi4gCLBMb0kiyK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/OnDMt/dJMb85vO133/YICsO5j6k7eVN9lwrZG9Uk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/QbgLi/dJMb81GXjgA/iIbshP2EUOzHh9dKVXn2I1/img.png?width=2292&amp;amp;height=496&amp;amp;face=0_0_2292_496&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/544&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/544&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bN8cDM/dJMb83ksZng/GnxiZit8oi4gCLBMb0kiyK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/OnDMt/dJMb85vO133/YICsO5j6k7eVN9lwrZG9Uk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/QbgLi/dJMb81GXjgA/iIbshP2EUOzHh9dKVXn2I1/img.png?width=2292&amp;amp;height=496&amp;amp;face=0_0_2292_496');&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;분명... slow쿼리를 모니터링하려고 했는데... 어째서...&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;return wrapPreparedStatement(preparedStatement, sql);​ 슬로쿼리 모니터링 인터페이스 제작슬로 쿼리를 모니터링 인터페이스를 만들기위해 어떤 작업을 해야 할까요? slow 쿼리는 어떻게 탐지할 수 있을까?Sl&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-end=&quot;360&quot; data-start=&quot;227&quot; data-ke-size=&quot;size16&quot;&gt;리플렉션은 간단히 말하면, 런타임 시점에 클래스의 구조를 조회하고, 메서드나 필드에 접근하여 동적으로 실행할 수 있는 기능입니다.&lt;br /&gt;이를 통해 일반적인 코드에서는 접근할 수 없는 private 메서드나 필드에도 접근이 가능합니다. 대표적으로 &lt;span&gt;&lt;span&gt;java.lang.Class&lt;/span&gt;&lt;/span&gt;, &lt;span&gt;&lt;span&gt;java.lang.reflect.Method&lt;/span&gt;&lt;/span&gt; 와 같은 API를 통해 클래스의 메타데이터를 조회하고 실행할 수 있습니다. 이러한 특성만 보면 &quot;JVM을 조작한다&quot;는 느낌 때문에 위험한 기술처럼 보일 수 있습니다. 하지만 정확히 말하면, 리플렉션은 JVM 자체를 변경하는 것이 아니라 JVM이 제공하는 메타데이터를 기반으로 객체를 동적으로 제어하는 기능에 가깝습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;360&quot; data-start=&quot;227&quot; data-ke-size=&quot;size26&quot;&gt;그렇다면, 리플렉션 기술은 왜 필요하고, 어디에서 사용할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;사실, 이 기술을 직접적으로 사용하는 일은 거의 없습니다.&lt;/i&gt; 대부분은 이미 구현되어 있는 프레임워크나 라이브러리를 통해 간접적으로 사용하게 됩니다. 그럼에도 불구하고 리플렉션을 알아야 하는 이유는 명확합니다. 우리가 사용하는 많은 기능들이, 내부적으로 리플렉션 위에서 동작하기 때문입니다. 예를 들어 &lt;b&gt;&lt;span&gt;&lt;span&gt;Spring Framework&lt;/span&gt;&lt;/span&gt; 의 DI, AOP, 애노테이션 기반 기능&lt;/b&gt;들은 모두 런타임에 객체를 분석하고 제어해야 하는데, 이 과정에서 리플렉션이 적극적으로 사용됩니다. 또한 &lt;u&gt;&lt;span&gt;&lt;span&gt;java.lang.reflect.Proxy&lt;/span&gt;&lt;/span&gt;&lt;/u&gt; 를 기반으로 한 동적 프록시 역시 리플렉션을 통해 메서드 호출을 가로채고 위임하는 구조로 동작합니다.&lt;/p&gt;
&lt;p data-end=&quot;496&quot; data-start=&quot;275&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같은 의문이 들 수 있습니다. &quot;이미 잘 만들어진 추상화가 있는데, 굳이 내부 구현까지 알아야 할까?&quot;&lt;br /&gt;이 질문에 대한 답은 명확합니다. 추상화는 편의를 제공하지만, 문제 상황에서는 원인을 숨기기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;496&quot; data-start=&quot;275&quot; data-ke-size=&quot;size16&quot;&gt;실제로 이러한 문제를 경험한 적이 있습니다. readOnly 트랜잭션이 적용되어 있다면 일반적으로 Replica DB로 라우팅될 것이라고 기대합니다. 하지만 예상과 달리 Primary DB가 사용되는 문제가 발생했습니다. 원인을 분석해보니, 트랜잭션 속성이 적용되기 전에 &lt;br /&gt;Connection이 먼저 생성되면서 라우팅이 잘못 이루어지고 있었습니다. 이 문제를 해결하기 위해,&lt;br /&gt;Connection 생성 시점을 트랜잭션 속성이 반영된 이후로 미루는 방식을 선택했습니다. 구체적으로 &lt;span&gt;&lt;span&gt;LazyConnectionDataSourceProxy&lt;/span&gt;&lt;/span&gt; 를 활용하여 올바른 시점에 Connection이 생성되도록 구조를 변경했습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;496&quot; data-start=&quot;275&quot; data-ke-size=&quot;size26&quot;&gt;이제 실제로 리플렉션이 어떻게 동작하는지 살펴보겠습니다.&lt;/h2&gt;
&lt;p data-end=&quot;496&quot; data-start=&quot;275&quot; data-ke-size=&quot;size16&quot;&gt;앞서 리플렉션은 런타임에 클래스의 구조를 조회하고, 메서드나 필드에 접근하여 동적으로 실행할 수 있는 기능이라고 설명했습니다.&lt;br /&gt;그렇다면 내부에서는 어떤 흐름으로 동작하고 있을까요?&lt;/p&gt;
&lt;p data-end=&quot;496&quot; data-start=&quot;275&quot; data-ke-size=&quot;size16&quot;&gt;리플렉션의 시작은 항상 &lt;span&gt;&lt;span&gt;java.lang.Class&lt;/span&gt;&lt;/span&gt; 객체로부터 시작합니다.&lt;/p&gt;
&lt;p data-end=&quot;360&quot; data-start=&quot;247&quot; data-ke-size=&quot;size16&quot;&gt;이는 객체를 생성할 때 클래스를 정의하는 것과 비슷한 개념으로 볼 수 있습니다.&lt;br /&gt;다만 중요한 차이는, 리플렉션에서는 &lt;b&gt;실제 객체 생성 여부와 관계없이 클래스의 구조에 접근할 수 있다는 점&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;413&quot; data-start=&quot;362&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 클래스가 있다고 가정해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1774871817025&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Class Person {
  private int num;

  public Persion(int num) {
   this.num = num;
  }
  
  public void methods(int a) {
   // 암튼 구현
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;496&quot; data-start=&quot;275&quot; data-ke-size=&quot;size16&quot;&gt;하지만 리플렉션을 사용하면, 객체를 생성하지 않더라도 Class 객체를 통해 클래스의 구조에 접근할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1774872139558&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Class&amp;lt;?&amp;gt; clazz = Person.class;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;496&quot; data-start=&quot;275&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 얻은 Class 객체를 통해 필드, 메서드, 생성자 정보를 조회할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;480&quot; data-start=&quot;444&quot; data-ke-size=&quot;size16&quot;&gt;리플렉션을 통해 접근할 수 있는 주요 요소는 크게 세 가지입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;526&quot; data-start=&quot;482&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;493&quot; data-start=&quot;482&quot; data-section-id=&quot;1pv6afn&quot;&gt;필드(Field)&lt;/li&gt;
&lt;li data-end=&quot;507&quot; data-start=&quot;494&quot; data-section-id=&quot;1fwmy02&quot;&gt;메서드(Method)&lt;/li&gt;
&lt;li data-end=&quot;526&quot; data-start=&quot;508&quot; data-section-id=&quot;1f9x6il&quot;&gt;생성자(Constructor)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;558&quot; data-start=&quot;533&quot; data-ke-size=&quot;size16&quot;&gt;각 요소는 서로 다른 역할을 가지고 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;627&quot; data-start=&quot;560&quot; data-ke-size=&quot;size16&quot;&gt;필드는 객체의 상태를 나타내며, 리플렉션을 통해 private 필드까지 접근하여 값을 조회하거나 변경할 수 있습니다.&lt;br /&gt;생성자는 객체를 생성하는 역할을 하며, 리플렉션을 통해 특정 생성자를 선택하여 동적으로 객체를 생성할 수 있습니다.&lt;br /&gt;하지만 실제로 리플렉션에서 가장 많이 사용되는 부분은 메서드입니다.&lt;br /&gt;메서드는 단순 조회를 넘어, invoke()를 통해 런타임에 직접 실행할 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;627&quot; data-start=&quot;560&quot; data-ke-size=&quot;size16&quot;&gt;리플렉션을 사용한 객체는 JVM에서 바로 실행되지 않습니다. 내부적으로는 Method.invoke() -&amp;gt; MethodAccessor.invoke() -&amp;gt; 실제 메서드 실행 이 되어집니다. 즉, 리플렉션은 바로 실행되는 것이 아니라, 중간 레이어를 통해 간접적으로 실행되는 구조입니다.&lt;/p&gt;
&lt;h2 data-end=&quot;627&quot; data-start=&quot;560&quot; data-ke-size=&quot;size26&quot;&gt;어째서 JVM으로 바로 실행하지 않고, 리플렉션은 중간과정을 거치는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리플렉션은 컴파일 시점에 구조가 확정되지 않기 때문에, JVM이 직접 최적화할 수 없고 중간 레이어를 거치게 됩니다.&lt;br /&gt;일반 메서드 호출과 리플렉션 호출을 비교해보겠습니다. 일반 메서드 호출은 컴파일 시점에 어떤 메서드가 실행될지 이미 확정됩니다.&lt;br /&gt;그렇기 때문에 JVM은 해당 호출에 대해 직접 최적화를 수행할 수 있습니다. &lt;br /&gt;반면, 리플렉션 호출은 어떤 메서드가 실행될지 런타임에 결정됩니다.&lt;br /&gt;즉, JVM 입장에서는 정적 정보가 부족한 상태입니다. 그렇다면 왜 invoke()로 바로 실행하지 못할까요?&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그 이유는 크게 세 가지입니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;첫 번째는 &lt;/span&gt;&lt;b&gt;타입 안정성 보장&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;입니다. &lt;/span&gt;리플렉션은 실행 시점에 객체와 파라미터를 전달받습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1774874579746&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;method.invoke(obj, args);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 JVM은 매번 다음과 같은 검사를 수행해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;641&quot; data-start=&quot;587&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;602&quot; data-start=&quot;587&quot; data-section-id=&quot;1iewbn4&quot;&gt;객체 타입이 올바른지&lt;/li&gt;
&lt;li data-end=&quot;619&quot; data-start=&quot;603&quot; data-section-id=&quot;lx032r&quot;&gt;매개변수 타입이 맞는지&lt;/li&gt;
&lt;li data-end=&quot;641&quot; data-start=&quot;620&quot; data-section-id=&quot;jlj8a5&quot;&gt;해당 메서드를 호출할 수 있는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;681&quot; data-start=&quot;643&quot; data-ke-size=&quot;size16&quot;&gt;즉, 컴파일 시점이 아니라 런타임에 모든 검증이 이루어집니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;두 번째는 &lt;/span&gt;&lt;b&gt;접근 제어 및 보안 문제&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;입니다. &lt;/span&gt;리플렉션은 private 메서드나 필드에도 접근이 가능합니다.&lt;br /&gt;이는 캡슐화를 깨는 접근이기 때문에 JVM 입장에서는 반드시 보안 체크가 필요합니다.&lt;br /&gt;&lt;br /&gt;즉, 검증 없이 바로 실행시키는 것은 위험합니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;세 번째는 &lt;/span&gt;&lt;b&gt;호출 대상의 불확정성&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;942&quot; data-start=&quot;864&quot; data-ke-size=&quot;size16&quot;&gt;리플렉션은 어떤 메서드가 실행될지 런타임에 결정됩니다. 이 구조에서는 JVM이 호출 대상을 미리 알 수 없기 때문에 최적화가 어렵습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;결국 이러한 이유들 때문에, 리플렉션은 바로 실행되지 않고 중간 계층을 거치게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774874600726&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Method.invoke()
 &amp;rarr; MethodAccessor.invoke()
   &amp;rarr; 실제 메서드 실행&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;p data-end=&quot;1113&quot; data-start=&quot;1070&quot; data-ke-size=&quot;size16&quot;&gt;이 중간 계층은 타입 검증, 접근 제어, 호출 위임과 같은 역할을 수행합니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;정리해보면, &lt;/span&gt;리플렉션은 단순히 느린 것이 아니라, 유연성을 얻기 위해 필수적인 검증과정을 포함하고 있는 구조라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;199&quot; data-start=&quot;121&quot; data-ke-size=&quot;size16&quot;&gt;다행인 점은, 리플렉션이 항상 느린 것은 아니라는 점입니다. JDK는 성능 문제를 인지하고 있기 때문에, 내부적으로 최적화를 수행합니다.초기에는 Native 방식으로 실행되지만, 일정 횟수 이상 호출되면 bytecode 기반 호출로 변경됩니다.&lt;br /&gt;이 과정에서 기존의 Native 호출은 보다 빠른 형태의 호출로 대체되며, 반복 호출이 많은 경우 성능을 개선할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 최적화 과정을 &lt;b&gt;Reflection Inflation&lt;/b&gt;이라고 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1774875144424&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;초기: NativeMethodAccessorImpl
 &amp;rarr; 이후: GeneratedMethodAccessor&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;1373&quot; data-start=&quot;1335&quot; data-ke-size=&quot;size26&quot;&gt;그렇다면, Reflection Inflation는 무엇일까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리플렉션은 앞서 살펴본 것처럼 중간 계층을 거쳐 실행되기 때문에 일반 메서드 호출보다 상대적으로 느릴 수 있습니다.&lt;br /&gt;JDK는 이러한 성능 문제를 완화하기 위해 내부적으로 최적화 전략을 사용합니다.&lt;/p&gt;
&lt;p data-end=&quot;312&quot; data-start=&quot;273&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;그 중 하나가 바로 &lt;b&gt;Reflection Inflation&lt;/b&gt;입니다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-end=&quot;402&quot; data-start=&quot;319&quot; data-ke-size=&quot;size16&quot;&gt;리플렉션은 처음 호출될 때는 Native 방식으로 실행됩니다.&lt;br /&gt;이 방식은 초기 실행 비용은 적지만, 반복 호출이 많아질수록 성능이 좋지 않습니다.&lt;/p&gt;
&lt;p data-end=&quot;492&quot; data-start=&quot;404&quot; data-ke-size=&quot;size16&quot;&gt;그래서 JDK는 일정 횟수 이상 동일한 리플렉션 호출이 발생하면,&lt;br /&gt;해당 호출을 보다 빠르게 처리하기 위해 바이트코드 기반 호출 방식으로 변경합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1774876206913&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;초기: NativeMethodAccessorImpl
 &amp;rarr; 이후: GeneratedMethodAccessor&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;648&quot; data-start=&quot;567&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 기존의 Native 기반 호출을, 더 빠른 형태의 accessor로 교체하는 과정을 Reflection Inflation이라고 합니다.&lt;br /&gt;이 구조를 보면 재미있는 점이 하나 있습니다. 처음부터 빠른 방식을 사용하지 않고, 왜 굳이 두 단계를 나누었을까요?&lt;/p&gt;
&lt;p data-end=&quot;744&quot; data-start=&quot;725&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 비용 때문입니다.&lt;br /&gt;바이트코드를 생성하는 작업 자체도 비용이 크기 때문에, 호출이 적은 경우에는 오히려 손해가 될 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;820&quot; data-start=&quot;810&quot; data-ke-size=&quot;size16&quot;&gt;그래서 JDK는 처음에는 가볍게 Native로 실행하고 반복 호출이 많아질 때만 최적화를 적용하는 방식으로 설계되어 있습니다.&lt;br /&gt;하지만 Reflection Inflation방식은 과거의 방식이고 최근 JDK에는 MethodHandle 기반으로 내부 구현이 변경되었습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;820&quot; data-start=&quot;810&quot; data-ke-size=&quot;size26&quot;&gt;어째서 Reflection Inflation방식대신 MethodHandle방식으로 변경되었는가?&lt;/h2&gt;
&lt;p data-end=&quot;553&quot; data-start=&quot;372&quot; data-ke-size=&quot;size16&quot;&gt;Reflection Inflation은 반복 호출 시 성능을 개선하기 위한 전략이었지만,&lt;br /&gt;근본적인 한계를 가지고 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;293&quot; data-start=&quot;259&quot; data-ke-size=&quot;size16&quot;&gt;첫 번째는 &lt;b&gt;JVM이 이해하기 어려운 구조&lt;/b&gt;라는 점입니다.&lt;br /&gt;리플렉션은 Method.invoke()를 통해 실행되며, 중간에 MethodAccessor와 같은 계층을 거치게 됩니다.&lt;br /&gt;이 구조는 JVM 입장에서 &quot;어떤 메서드가 호출되는지&quot;를 명확하게 알기 어렵기 때문에, 인라이닝과 같은 최적화를 적용하기 힘듭니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, &lt;/span&gt;최적화의 한계가 존재합니다.&lt;/p&gt;
&lt;p data-end=&quot;500&quot; data-start=&quot;474&quot; data-ke-size=&quot;size16&quot;&gt;두 번째는 &lt;b&gt;복잡한 내부 구현 구조&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;525&quot; data-start=&quot;502&quot; data-ke-size=&quot;size16&quot;&gt;Reflection Inflation은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;568&quot; data-start=&quot;526&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;539&quot; data-start=&quot;526&quot; data-section-id=&quot;1c6vhv1&quot;&gt;Native 호출&lt;/li&gt;
&lt;li data-end=&quot;552&quot; data-start=&quot;540&quot; data-section-id=&quot;1bx3o2c&quot;&gt;바이트코드 생성&lt;/li&gt;
&lt;li data-end=&quot;568&quot; data-start=&quot;553&quot; data-section-id=&quot;ur6xw5&quot;&gt;accessor 교체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;592&quot; data-start=&quot;570&quot; data-ke-size=&quot;size16&quot;&gt;와 같은 여러 단계를 포함하고 있습니다.&lt;br /&gt;이러한 구조는 유지보수 비용이 높고, JVM 내부 구현을 복잡하게 만드는 원인이 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;645&quot; data-start=&quot;594&quot; data-ke-size=&quot;size16&quot;&gt;세 번째는 &lt;b&gt;일관되지 않은 성능 특성&lt;/b&gt;입니다.&lt;br /&gt;초기에는 Native 방식으로 실행되고, 일정 횟수 이후에만 최적화가 적용됩니다.&lt;br /&gt;즉, 호출 횟수에 따라 성능이 달라지는 구조이며, 예측하기 어려운 성능 특성을 가지게 됩니다.&lt;br /&gt;이러한 문제를 해결하기 위해 등장한 것이 MethodHandle입니다.&lt;br /&gt;MethodHandle은 리플렉션과 달리 JVM이 이해할 수 있는 형태로 호출을 표현합니다.&lt;/p&gt;
&lt;h2 data-end=&quot;645&quot; data-start=&quot;594&quot; data-ke-size=&quot;size26&quot;&gt;그렇다면, MethodHandle은 어떻게 동작하길래 JVM이 이해할 수 있는 형태로 표현하는 걸까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MethodHandle은 단순한 메타데이터가 아니라 이미 호출 대상과 타입이 확정된 실행 가능한 참조로 동작합니다.&lt;br /&gt;코드로 확인해봅시다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리플렉션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리플렉션 방식에서는 다음과 같이 코드가 작성이 되었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1774877942598&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Person {
    public void hello(int a) {
        System.out.println(&quot;hello &quot; + a);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1774877964540&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Method method = Person.class.getMethod(&quot;hello&quot;, int.class);
Person p = new Person();
// 실행
method.invoke(p, 10);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리플렉션은 런타임 시점에서 메서드가 실행됩니다. 그렇다는 이야기는, 컴파일 시점에는 호출 대상이 확정되지 않기 때문에 보안 검사와 호출 대상의 불확실성을 런타임에 처리해야 한다는 의미입니다. 이 과정에서 리플렉션은 중간 계층을 두어 실행되는 구조를 가지게 되었고,&lt;br /&gt;그 결과 매번 해석과 검증이 필요한 문제가 발생했습니다. 이러한 성능 문제를 완화하기 위해 등장한 것이 &lt;b&gt;Reflection Inflation&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;463&quot; data-ke-size=&quot;size16&quot;&gt;리플렉션은 처음에는 Native 방식으로 실행되지만, 일정 횟수 이상 호출이 발생하면 바이트코드 기반 호출로 전환됩니다.&lt;br /&gt;이를 통해 반복 호출에 대한 성능을 일부 개선할 수 있었습니다. &lt;br /&gt;하지만 이 방식은 근본적인 해결책은 아니었습니다. 여전히 중간 계층을 거치는 구조이기 때문에&lt;br /&gt;추가적인 호출 비용이 존재했고, JVM이 직접 최적화하기 어려운 구조라는 한계를 가지고 있었습니다.&lt;br /&gt;&lt;br /&gt;이러한 한계를 해결하기 위해 등장한 것이 MethodHandle입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MethodHandle 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MethodHandle은 리플렉션과 달리, 호출 대상과 타입 정보를 미리 확정된 형태로 표현합니다.&lt;br /&gt;이 덕분에 JVM은 해당 호출을 더 명확하게 이해할 수 있고, 인라이닝과 같은 최적화를 적용할 수 있게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1774879277218&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

MethodHandles.Lookup lookup = MethodHandles.lookup();

MethodHandle mh = lookup.findVirtual(
    Person.class,
    &quot;hello&quot;,
    MethodType.methodType(void.class, int.class)
);

Person p = new Person();

// 실행
mh.invokeExact(p, 10);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1567&quot; data-start=&quot;1532&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 되면 호출 방식 자체가 달라지게 됩니다. 리플렉션은 실행 시점마다 호출 대상을 해석하고 검증하는 구조입니다.&lt;br /&gt;즉, 매번 &quot;무엇을 호출할지&quot;를 확인하는 과정이 필요합니다. 반면 MethodHandle은 메서드와 타입 정보가 이미 결합된 상태로 생성되기 때문에, 호출 시점에는 추가적인 해석 과정이 필요하지 않습니다.&lt;/p&gt;
&lt;p data-end=&quot;1567&quot; data-start=&quot;1532&quot; data-ke-size=&quot;size16&quot;&gt;핵심 차이는 다음과 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1774879641881&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Reflection
문자열 &amp;rarr; 메서드 탐색 &amp;rarr; 실행 시 검증 &amp;rarr; 호출

MethodHandle
메서드 + 타입 &amp;rarr; 생성 시 확정 &amp;rarr; 바로 호출&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 차이로 인해, MethodHandle은 JVM이 호출 구조를 명확하게 인식할 수 있는 형태로 표현됩니다.&lt;br /&gt;즉, 단순한 동적 실행이 아니라, 정적인 호출에 가까운 구조로 변환된 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;647&quot; data-start=&quot;628&quot; data-ke-size=&quot;size16&quot;&gt;그 결과 JVM은 해당 호출에 대해&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;677&quot; data-start=&quot;649&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;655&quot; data-start=&quot;649&quot; data-section-id=&quot;cwbq85&quot;&gt;인라이닝&lt;/li&gt;
&lt;li data-end=&quot;665&quot; data-start=&quot;656&quot; data-section-id=&quot;qdm0ye&quot;&gt;JIT 최적화&lt;/li&gt;
&lt;li data-end=&quot;677&quot; data-start=&quot;666&quot; data-section-id=&quot;yrbcx9&quot;&gt;호출 경로 단순화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;702&quot; data-start=&quot;679&quot; data-ke-size=&quot;size16&quot;&gt;와 같은 최적화를 적용할 수 있게 됩니다.&lt;/p&gt;
&lt;h2 data-end=&quot;702&quot; data-start=&quot;679&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리플렉션부터 MethodHandle까지 살펴보았습니다. 리플렉션의 한계와, 이를 어떻게 보완하고 개선해왔는지에 대해 학습할 수 있었습니다. 특히 단순히 &quot;리플렉션은 느리다&quot;는 수준이 아니라, 왜 그런 구조를 가지게 되었는지, 그리고 JDK가 이를 어떻게 해결하려 했는지를 이해할 수 있었다는 점이 의미 있었습니다. 다만 아쉬운 점도 있습니다. MethodHandle에 대해서는 개념적으로 이해하는 수준에 그쳤고,&lt;br /&gt;아직 내부 동작이나 활용 방식까지 깊게 다루지는 못했습니다. 아무래도 처음 접한 개념이다 보니, 완전히 체화하기에는 시간이 조금 더 필요할 것 같습니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/560</guid>
      <comments>https://b-programmer.tistory.com/560#entry560comment</comments>
      <pubDate>Mon, 30 Mar 2026 23:13:05 +0900</pubDate>
    </item>
    <item>
      <title>카프카의 설정은 진짜일까..?! - Acks 편</title>
      <link>https://b-programmer.tistory.com/558</link>
      <description>&lt;figure id=&quot;og_1774512802859&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;EDA 패턴을 적용해보자.(feat.kafka)&quot; data-og-description=&quot;이전 포스트에서 이어져서 진행이 되어집니다. 선착순 쿠폰 발급기 개발당신의 동시성 테스트가 원하는 결과가 나오지 않는 이유TL;DR: 낙관락과 비관락을 고르는 기준에 대해 설명합니다.배경&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/557&quot; data-og-url=&quot;https://b-programmer.tistory.com/557&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/URyLn/dJMb86nXgyJ/iUWkAr0lHVCXeGcGQO6WK1/img.png?width=800&amp;amp;height=432&amp;amp;face=0_0_800_432,https://scrap.kakaocdn.net/dn/d6yflA/dJMb85WS2hH/cspnWmX9IRS8TA7ksiyprk/img.png?width=800&amp;amp;height=432&amp;amp;face=0_0_800_432,https://scrap.kakaocdn.net/dn/IsOT5/dJMb8U8TiD1/IojkUw0AVOcsyMYQg3GfK0/img.png?width=1558&amp;amp;height=1456&amp;amp;face=0_0_1558_1456&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/557&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/557&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/URyLn/dJMb86nXgyJ/iUWkAr0lHVCXeGcGQO6WK1/img.png?width=800&amp;amp;height=432&amp;amp;face=0_0_800_432,https://scrap.kakaocdn.net/dn/d6yflA/dJMb85WS2hH/cspnWmX9IRS8TA7ksiyprk/img.png?width=800&amp;amp;height=432&amp;amp;face=0_0_800_432,https://scrap.kakaocdn.net/dn/IsOT5/dJMb8U8TiD1/IojkUw0AVOcsyMYQg3GfK0/img.png?width=1558&amp;amp;height=1456&amp;amp;face=0_0_1558_1456');&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;EDA 패턴을 적용해보자.(feat.kafka)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이전 포스트에서 이어져서 진행이 되어집니다. 선착순 쿠폰 발급기 개발당신의 동시성 테스트가 원하는 결과가 나오지 않는 이유TL;DR: 낙관락과 비관락을 고르는 기준에 대해 설명합니다.배경&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 글에 간단하게 outbox패턴을 얘기하면서 카프카의 설정을 살짝 언급한 적이 있습니다. 사실 outbox패턴이 없어도 카프카는 메시지를 다시 읽을 수 있었습니다. 바로 @RetryalbeTopic이라는 어노테이션을 이용하면 producer에서 발송된 메시지를 다시 읽을 수 있게 유도할 수 있었습니다. 그렇다면, 이러한 설정들이 실제로 어떻게 동작을 할까요? 마지막에도 언급했듯이 제 목표는 native 하게 kafka를 사용하는 것이 목표입니다. 그 이후에는 CDC까지 사용해서 outbox를 풀링 하는 방식이 아닌 DB내부의 로그를 읽는 방식으로 변경할 예정입니다. 한번 해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작은 간단하게 producer의 설정들부터 들어가 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ack를 간단하게 설명하면, 어떻게 요청하는 방법에 대해 정의를 하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;acsks=0:&lt;/b&gt;&lt;br /&gt;- 가장 빠르게 전송할 수 있는 방식이지만, 유실 위험이 가장 큽니다.&amp;nbsp;&lt;br /&gt;- 지연&amp;nbsp;시간은&amp;nbsp;줄일&amp;nbsp;수&amp;nbsp;있지만,&amp;nbsp;메시지가&amp;nbsp;broker에&amp;nbsp;실제로&amp;nbsp;저장되었는지는&amp;nbsp;보장하지&amp;nbsp;않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;acsks=1:&lt;/b&gt;&lt;br /&gt;- 리더 broker가&amp;nbsp;메시지를&amp;nbsp;받으면&amp;nbsp;성공으로&amp;nbsp;판단합니다.&lt;br /&gt;- follower&amp;nbsp;replica에&amp;nbsp;반영될&amp;nbsp;때까지는&amp;nbsp;기다리지&amp;nbsp;않습니다.&lt;br /&gt;- 따라서&amp;nbsp;리더까지&amp;nbsp;전달되었다는&amp;nbsp;보장은&amp;nbsp;있지만,&amp;nbsp;리더 장애가&amp;nbsp;발생하면&amp;nbsp;유실&amp;nbsp;가능성이&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;acks=all(-1):&lt;/b&gt;&lt;br /&gt;- 리더와 ISR(in-sync replicas)에 기록되어야 성공으로 판단합니다.&lt;br /&gt;- 가장&amp;nbsp;안전한&amp;nbsp;방식이지만,&amp;nbsp;상대적으로&amp;nbsp;지연&amp;nbsp;시간이&amp;nbsp;길어질&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;broker가 여러 대가 있어야 의미 있는 테스트를 할 수 있는 걸까요? 사실은 그렇지 않습니다. acks=0을 다시 읽어보면 &lt;br /&gt;메시지가 &quot;&lt;b&gt;broker에 저장되었는지는 보장하지 않는다&lt;/b&gt;&quot;라고 하였습니다. 그렇다는 이야기는 broker한대로도 충분히 테스트가 가능할거 같습니다. 만약, 제가 원하는 테스트라고 되었다는 가정하에 말하자면, 아무래도 유실이 있을 거라 예상합니다.&lt;br /&gt;하지만, 일반적인 상황에서는 아무런 변화가 없을겁니다. broker에 강제적으로 장애를 심어줘야 하지 않을까 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저&amp;nbsp; broker를 한개만 두고 실습해 보죠.&lt;br /&gt;다음과 같은 시나리오로 테스트를 수행했습니다. 각 설정별로 총 50만 건의 메시지를 전송했으며, 전송은 5만 건 단위의 배치 방식으로 진행했습니다. 전체 전송량의 50%인 25만 건을 보낸 시점 이후 장애를 주입했고, 다음 배치를 전송하는 도중 브로커를 3초간 중지한 뒤 재시작했습니다. 전송 실패가 발생한 배치는 브로커 복구 후 재시도하도록 구성했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBivVg/dJMcafMOYcc/PbFF8QpSXsk3h5MJQQaXGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBivVg/dJMcafMOYcc/PbFF8QpSXsk3h5MJQQaXGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBivVg/dJMcafMOYcc/PbFF8QpSXsk3h5MJQQaXGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBivVg%2FdJMcafMOYcc%2FPbFF8QpSXsk3h5MJQQaXGk%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;728&quot; height=&quot;312&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 테스트를 통해 acks=0과 acks=1의 정합성 차이를 직접적으로 판단하기는 어려웠습니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;두 설정 모두 장애 구간에서 약 5만 건의 전송 실패가 발생했으며, 이후 재시도 과정에서 중복 메시지가 관찰되었습니다. 특히 acks=1의 경우, acks=0 대비 약 2~3배 더 많은 중복 메시지가 발생했습니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;다만 이 결과만으로 acks=1이 acks=0보다 정합성을 더 잘 보장한다고 단정하기는 어렵습니다. 단일 브로커 환경에서는 복제가 존재하지 않기 때문에, acks 설정에 따른 내구성(정합성) 차이가 실제로 드러나지 않기 때문입니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이번 테스트를 통해 확인할 수 있었던 것은 acks 설정에 따른 정합성 차이 자체보다는, 장애 이후 재시도 과정에서 중복 메시지가 발생하는 양상이었습니다. &lt;/span&gt;&lt;span&gt;따라서 본 결과는 정합성에 대한 직접적인 검증이라기보다는, 단일 브로커 환경에서 acks 설정에 따라 중복 및 재시도 양상이 어떻게 나타나는지를 보여주는 참고 수준의 결과로 보는 것이 적절합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속 안정성을 언급해왔었는데 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;acks=1&lt;/span&gt;이 중복값이 많다는건 어떤 안전성을 말하고 있는걸까요?&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;여기서 말하고 있는 안전성이란, 유실 가능성을 말하고 있습니다. &lt;br /&gt;중복 메시지의 증가는 동일한 데이터를 여러 번 처리해야 함을 의미하며, 네트워크 전송, 디스크 I/O, 컨슈머 처리량 측면에서 추가적인 비용을 발생시킬 수 있습니다. 따라서 재시도로 인해 중복 메시지가 증가하는 상황은 시스템 전체 관점에서 성능 저하 요인으로 작용할 수 있습니다. 사실 이것은 kafka의 의도적인 트레이드 오프라고 볼 수 있습니다.&lt;br /&gt;중복 메시지를 늘려 유실 가능성이 줄게 된다면, 최종적으로는 최소 한 번 이상은 반드시 처리가 되어집니다. 다른 말로는 at-least-once라고 합니다.&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그렇다면, acks=0에서 중복데이터가 발생한것은 무엇때문일까요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;acks=0에서 중복 데이터가 발생한 이유는, 장애가 발생한 시점에 일부 메시지가 이미 브로커에 전달되었을 가능성이 있기 때문입니다.&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;1320&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mxv3m/dJMcahw5zH4/qypdn1krNUJf6PhFQPJHik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mxv3m/dJMcahw5zH4/qypdn1krNUJf6PhFQPJHik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mxv3m/dJMcahw5zH4/qypdn1krNUJf6PhFQPJHik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmxv3m%2FdJMcahw5zH4%2Fqypdn1krNUJf6PhFQPJHik%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;585&quot; height=&quot;264&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;producer가 broker쪽으로 메시지를 보냅니다. 여기에 만약 acks=0인 상태라면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tPNZL/dJMcabcw2hZ/pNvRbsluqANiJreIR6FPK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tPNZL/dJMcabcw2hZ/pNvRbsluqANiJreIR6FPK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tPNZL/dJMcabcw2hZ/pNvRbsluqANiJreIR6FPK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtPNZL%2FdJMcabcw2hZ%2FpNvRbsluqANiJreIR6FPK0%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;604&quot; height=&quot;264&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;broker에 확인하지 않고 지나갑니다. producer가 해당 메시지가 실제로 broker에 정상 반영되었는지 확인하지 못한 채 다음 전송으로 넘어간다는 뜻입니다. &lt;br /&gt;만약 broker가 중지된 상태라면, producer broker로 메시지를 정상적으로 전달하지 못할 수 있습니다. 이후 broker 다시 활성화되더라도, producer 입장에서는 broker 장애 시점에 해당 메시지가 실제로 전송되었는지, 또는 전송되지 않았는지를 정확히 알 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 저장 여부가 불확실한 상태에서는, 유실을 방지하기 위해 동일한 메시지 또는 동일한 배치를 다시 전송하게 됩니다. &lt;br /&gt;그 결과, 장애 직전에 이미 일부 저장된 메시지까지 함께 재전송되면서 중복 메시지가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 단일 브로커에서의 acks동작 결과에 대해 이해를 해보았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Kafka는 브로커를 단일로만 구성하는 시스템이 아닙니다. 필요에 따라 여러 대의 브로커로 클러스터를 구성할 수 있으며, 이 환경에서 acks=1과 acks=all의 차이가 보다 분명하게 드러납니다. 단일 브로커 환경에서는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;acks=1&lt;/span&gt;과 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;acks=all&lt;/span&gt; 모두 사실상 리더 브로커 1대의 응답만 기준이 되기 때문에, 두 설정의 차이를 충분히 확인하기 어렵습니다. 반면 다중 브로커 환경에서는 리더 외에 복제본(replica)이 함께 존재하므로, acks=1은 리더 브로커의 저장만 확인한 뒤 응답하지만, acks=all은 in-sync&amp;nbsp;replica(ISR)까지&amp;nbsp;반영된&amp;nbsp;이후&amp;nbsp;응답하게&amp;nbsp;됩니다.&lt;br /&gt;&lt;br /&gt;따라서 다중 브로커 환경에서는 acks=1과 acks=all이 단순한 응답 속도 차이를 넘어, 장애 상황에서 데이터 유실 가능성과 정합성 보장 수준에 어떤 차이를&amp;nbsp;만드는지를&amp;nbsp;보다&amp;nbsp;명확하게&amp;nbsp;이해할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이제 acks=1 vs acks=all을 비교해보자&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;두 개 모두 replica라는 용어가 등장합니다. replica는 메인 브로커가 아닌 서브 브로커라고 생각하시면 됩니다.&lt;br /&gt;그렇다면, 어떤 점이 다를까요? 저는 이번 테스트를 위해 kafka를 2대를 더 설치하기로 하였습니다.&lt;br /&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhkmV3/dJMcaf65qmj/MQHIkR8QoPB53t2FKOe450/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhkmV3/dJMcaf65qmj/MQHIkR8QoPB53t2FKOe450/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhkmV3/dJMcaf65qmj/MQHIkR8QoPB53t2FKOe450/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhkmV3%2FdJMcaf65qmj%2FMQHIkR8QoPB53t2FKOe450%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;471&quot; height=&quot;301&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;대략 요런 느낌이 되는거죠.&amp;nbsp;&lt;br /&gt;테스트는 2가지로 진행이 되어질 예정입니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정상적으로 동작하였을때, 어떤 방법이 더 빠른지, 안정적인지 점검해봅시다.&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지는 5000건으로 진행하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KW2g8/dJMcag505gV/a92nVZQTkvwAnar5qfzOgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KW2g8/dJMcag505gV/a92nVZQTkvwAnar5qfzOgK/img.png&quot; data-alt=&quot;5회 평균에서도 acks=0이 가장 빠르고, acks=all이 가장 느렸다. 하지만 내구성 차이는 장애 상황에서 따로 확인해야 한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KW2g8/dJMcag505gV/a92nVZQTkvwAnar5qfzOgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKW2g8%2FdJMcag505gV%2Fa92nVZQTkvwAnar5qfzOgK%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;1714&quot; height=&quot;702&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;5회 평균에서도 acks=0이 가장 빠르고, acks=all이 가장 느렸다. 하지만 내구성 차이는 장애 상황에서 따로 확인해야 한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 결과는 acks=0이 Throughput이 높다는 의미라기보다는, &lt;br /&gt;&lt;/span&gt;&lt;span&gt;브로커의 응답을 기다리지 않는 방식으로 동작하기 때문에 &lt;/span&gt;&lt;span&gt;producer 관점에서의 latency가 낮게 나타난 것으로 해석할 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;패킷 수를 비교해보면 acks=0은 약 7,000개 수준인 반면, &lt;/span&gt;&lt;span&gt;acks=1 및 acks=all은 약 16,000개로 약 2.5배 더 많은 패킷이 발생했습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이는 acks=1과 acks=all이 브로커의 응답을 포함한 request-response 구조로 동작하기 때문에,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;왕복 통신 및 추가적인 네트워크 비용이 발생한 결과로 볼 수 있습니다. &lt;/span&gt;&lt;span&gt;즉, acks 설정에 따른 차이는 단순한 속도 차이가 아니라, &lt;br /&gt;&lt;/span&gt;&lt;span&gt;응답 대기 여부에 따른 latency와 네트워크 비용의 트레이드오프로 해석하는 것이 적절합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;그렇다면, 장애 상황에서는 어떻게 변화할까요?&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&lt;span&gt;레플리카 broker을 사용하는 acks는 1과 all입니다. 그렇다는건 레플리카를 사용하지 않는 acks=0은 어떻게 동작이 되어질까요? &lt;br /&gt;한번 확인해봅시다.&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&lt;span&gt;테스트는 leader가 죽었을때와 follow가 죽었을때로 비교가 되어집니다.&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&lt;span&gt;리더가 죽게되어지면 wireshark를 통해 다음과 같이 보여집니다.&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1814&quot; data-origin-height=&quot;1218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nwm6J/dJMcaibHVZC/3a2rFoX19CcGE12ENNkzUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nwm6J/dJMcaibHVZC/3a2rFoX19CcGE12ENNkzUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nwm6J/dJMcaibHVZC/3a2rFoX19CcGE12ENNkzUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnwm6J%2FdJMcaibHVZC%2F3a2rFoX19CcGE12ENNkzUK%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;647&quot; height=&quot;434&quot; data-origin-width=&quot;1814&quot; data-origin-height=&quot;1218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&lt;span&gt;이렇게 발생하는 이유는 producer입장에서는 기존 리더가 죽었는지 모르는 상황이라는 뜻입니다. 그래서 기존리더에게 요청을 계속 보내는 것을 확인 할 수 있습니다. 메시지는 3만건씩을 발송하였습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dD7kV4/dJMcahX8122/gaV97C2lAKU2eRKbQ5y2V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dD7kV4/dJMcahX8122/gaV97C2lAKU2eRKbQ5y2V1/img.png&quot; data-alt=&quot;leader가 죽으면 acks=0은 빠르지만 큰 유실이 생기고, acks=1은 거의 유지되며, acks=all은 느리지만 이번 5회에서는 전량 보존됐다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dD7kV4/dJMcahX8122/gaV97C2lAKU2eRKbQ5y2V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdD7kV4%2FdJMcahX8122%2FgaV97C2lAKU2eRKbQ5y2V1%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;1630&quot; height=&quot;728&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;leader가 죽으면 acks=0은 빠르지만 큰 유실이 생기고, acks=1은 거의 유지되며, acks=all은 느리지만 이번 5회에서는 전량 보존됐다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;놀랍게도 acks=0은 11,800건이 유실이 되었으며, 약 1/3정도가 유실이 되었지만 latency는 여전히 가장 빠른것을 확인 할 수 있었습니다.&lt;br /&gt;acks=1도 약간의 유실이 있긴하지만, 나름 괜찮은 대안이 된다고 여겨집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 replica에 장애가 발생한다면 어떻게 되어질까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZhxTL/dJMcacbtbGG/fymbwsCT1DOmUsHrg9LFc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZhxTL/dJMcacbtbGG/fymbwsCT1DOmUsHrg9LFc1/img.png&quot; data-alt=&quot;요약: follower 1대가 죽는 조건에서는 acks=0, 1, all 모두 전량 적재됐다. 차이는 주로 속도에서만 나타났다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZhxTL/dJMcacbtbGG/fymbwsCT1DOmUsHrg9LFc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZhxTL%2FdJMcacbtbGG%2FfymbwsCT1DOmUsHrg9LFc1%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;800&quot; height=&quot;346&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;요약: follower 1대가 죽는 조건에서는 acks=0, 1, all 모두 전량 적재됐다. 차이는 주로 속도에서만 나타났다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;리더와 달리 follow에 장애가 발생했을때는 유실이 되어지는것이 존재하지 않았습니다. 하지만 평균 실행 속도가 1초가량 늘었다는것을 확인할 수 있었습니다.&lt;br /&gt;이걸로 kafka의 성능은 leader의 장애 여부에 따라 차이가 존재한다는것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 producer쪽 전체를 전부 작성하고 싶었지만 ack하나만 적었을 뿐인데도 생각보다 길게 적게 되었네요. 정상 상태에서는 acks=0가 가장 빠르지만, leader 장애까지 고려하면 acks=all이 가장 안전하였습니다. 또한, acks=1은 성능과 내구성 사이의 중간 지점이지만, leader 장애에서 완전 무손실을 보장하지는 못한다는것을 확인하였습니다.&lt;br /&gt;&lt;br /&gt;참고로 테스트는 kcat과 tcpdump를 이용해 ai와 함께 테스트 하였습니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/558</guid>
      <comments>https://b-programmer.tistory.com/558#entry558comment</comments>
      <pubDate>Fri, 27 Mar 2026 23:56:47 +0900</pubDate>
    </item>
    <item>
      <title>EDA 패턴을 적용해보자.(feat.kafka)</title>
      <link>https://b-programmer.tistory.com/557</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스트에서 이어져서 진행이 되어집니다.&lt;/p&gt;
&lt;figure id=&quot;og_1774413133393&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;선착순 쿠폰 발급기 개발&quot; data-og-description=&quot;당신의 동시성 테스트가 원하는 결과가 나오지 않는 이유TL;DR: 낙관락과 비관락을 고르는 기준에 대해 설명합니다.배경그 전에도 낙관락, 비관락을 해봤기때문에 금방할 줄 알았다.하지만 아니&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/551&quot; data-og-url=&quot;https://b-programmer.tistory.com/551&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kemil/dJMb83ksvsU/C0loAc4QK5cf2NzACjxih0/img.png?width=800&amp;amp;height=432&amp;amp;face=0_0_800_432,https://scrap.kakaocdn.net/dn/ckqBzO/dJMb83Sipl8/zBgdrkTDTFjO9XRKNW4DUK/img.png?width=800&amp;amp;height=432&amp;amp;face=0_0_800_432,https://scrap.kakaocdn.net/dn/c06J2N/dJMb85vOxTZ/ONaOk3FtXcHwMEC5vrjBYK/img.png?width=500&amp;amp;height=356&amp;amp;face=0_0_500_356&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/551&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/551&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kemil/dJMb83ksvsU/C0loAc4QK5cf2NzACjxih0/img.png?width=800&amp;amp;height=432&amp;amp;face=0_0_800_432,https://scrap.kakaocdn.net/dn/ckqBzO/dJMb83Sipl8/zBgdrkTDTFjO9XRKNW4DUK/img.png?width=800&amp;amp;height=432&amp;amp;face=0_0_800_432,https://scrap.kakaocdn.net/dn/c06J2N/dJMb85vOxTZ/ONaOk3FtXcHwMEC5vrjBYK/img.png?width=500&amp;amp;height=356&amp;amp;face=0_0_500_356');&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;선착순 쿠폰 발급기 개발&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;당신의 동시성 테스트가 원하는 결과가 나오지 않는 이유TL;DR: 낙관락과 비관락을 고르는 기준에 대해 설명합니다.배경그 전에도 낙관락, 비관락을 해봤기때문에 금방할 줄 알았다.하지만 아니&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;초기 구현에서는 쿠폰 재고를 DB에서 직접 관리했습니다. 발급 요청이 들어오면 트랜잭션 안에서 재고를 확인하고 차감하는 구조였기 때문에, 정합성 측면에서는 비교적 안전했습니다. 실제로 초과 발급도 발생하지 않았습니다. 하지만 대량 트래픽 환경에서는 다른 문제가 드러났습니다. 특정 쿠폰에 요청이 집중되자 동일한 재고 데이터에 대한 동시 접근이 늘어났고, 결국 DB 락 경합이 병목으로 이어졌습니다.&lt;br /&gt;즉, 정합성은 확보했지만 성능 비용이 커지는 구조였던 것입니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;발급&amp;nbsp;재고&amp;nbsp;처리를&amp;nbsp;DB&amp;nbsp;락&amp;nbsp;중심에서&amp;nbsp;Redis&amp;nbsp;중심으로&amp;nbsp;옮기기&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가장&amp;nbsp;먼저,&amp;nbsp;현재&amp;nbsp;DB&amp;nbsp;락&amp;nbsp;중심의&amp;nbsp;재고&amp;nbsp;처리&amp;nbsp;방식을&amp;nbsp;Redis&amp;nbsp;중심으로&amp;nbsp;변경해보려고&amp;nbsp;합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 Redis가 더 적합하다고 판단했는가&lt;/b&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;현재 시나리오의 핵심 문제는 정합성이 아니라 처리량과 응답 성능이었습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;실제로 현재 구조는 비관락을 통해 초과 발급을 방지하고 있었고, 재고 정합성도 정상적으로 유지되고 있었습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;즉, 재고가 틀어지는 문제는 이미 어느 정도 해결된 상태였습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;하지만 문제는 그다음이었습니다. 쿠폰 발급 요청이 하나의 쿠폰에 집중되자, 모든 요청이 동일한 coupon row에 접근하게 되었고, 비관락 기반 구조에서는 이 요청들이 사실상 순차적으로 처리될 수밖에 없었습니다. 그 결과 DB는 재고 저장소인 동시에 동시성 제어 지점이 되었고, 결국 락 경합이 성능 병목으로 이어졌습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;이 지점에서 Redis가 더 적합하다고 판단했습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;Redis는 메모리 기반 저장소이기 때문에 DB보다 훨씬 빠르게 접근할 수 있고, DECR 같은 원자적 연산을 통해 재고 차감도 매우 가볍게 처리할 수 있습니다. 무엇보다 중요한 점은,&lt;/span&gt;&lt;u&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;DB처럼 특정 row에 대한 락 대기를 길게 유발하지 않으면서도 동시성 제어를 수행할 수 있다는 점&lt;/u&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;입니다. 즉, 현재 문제는 &quot;정합성을 어떻게 맞출 것인가&quot;가 아니라 &quot;정합성은 유지한 채, 더 많은 요청을 더 빠르게 처리할 수 있는가&quot;였고, 그 관점에서 Redis는 DB보다 훨씬 적합한 선택지라고 판단했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QqHZ0/dJMcafeMbmE/5SIKWXa15JukGCjZRAmPjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QqHZ0/dJMcafeMbmE/5SIKWXa15JukGCjZRAmPjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QqHZ0/dJMcafeMbmE/5SIKWXa15JukGCjZRAmPjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQqHZ0%2FdJMcafeMbmE%2F5SIKWXa15JukGCjZRAmPjk%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;624&quot; height=&quot;331&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;818&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;즉,&amp;nbsp;지금&amp;nbsp;구조는&amp;nbsp;DB가&amp;nbsp;재고&amp;nbsp;관리와&amp;nbsp;동시성&amp;nbsp;제어를&amp;nbsp;모두&amp;nbsp;담당하는&amp;nbsp;형태입니다.&lt;br /&gt;이 방식은 초과 발급을 막는 데는 효과적이었지만, 요청이 많이 몰리는 상황에서는 특정 쿠폰 row에 대한 락 경합이 발생하면서 성능 병목으로 이어질 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwugpl/dJMcaaR3Irt/WVqYIu9nN6XkxElfNGygBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwugpl/dJMcaaR3Irt/WVqYIu9nN6XkxElfNGygBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwugpl/dJMcaaR3Irt/WVqYIu9nN6XkxElfNGygBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwugpl%2FdJMcaaR3Irt%2FWVqYIu9nN6XkxElfNGygBK%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;546&quot; height=&quot;216&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;424&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;그래서 이 부분을 Redis 중심 구조로 변경할 예정입니다.&lt;br /&gt;앞으로는 DB에서 직접 락을 잡아 재고를 차감하는 대신, Redis에서 재고를 원자적으로 감소시키고, DB는 최종 이력 저장 역할에 더 집중하도록 개선하려고 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제부터 DB에서 재고를 차감하는 것이 아닌 Redis에서 재고를 차감하는 방식으로 변경하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJMkEw/dJMcaivJyez/WB24dKEAUwj2Dw0uLhJxV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJMkEw/dJMcaivJyez/WB24dKEAUwj2Dw0uLhJxV1/img.png&quot; data-alt=&quot;재고가 마이너스가 된다면, 강제적으로 0으로 바꿔주는 코드입니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJMkEw/dJMcaivJyez/WB24dKEAUwj2Dw0uLhJxV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJMkEw%2FdJMcaivJyez%2FWB24dKEAUwj2Dw0uLhJxV1%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;234&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;재고가 마이너스가 된다면, 강제적으로 0으로 바꿔주는 코드입니다.&lt;/figcaption&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;822&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuvgV6/dJMcaduoQJw/xJBJXgs8Krxb5ELqZoEgYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuvgV6/dJMcaduoQJw/xJBJXgs8Krxb5ELqZoEgYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuvgV6/dJMcaduoQJw/xJBJXgs8Krxb5ELqZoEgYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuvgV6%2FdJMcaduoQJw%2FxJBJXgs8Krxb5ELqZoEgYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;550&quot; data-origin-width=&quot;822&quot; data-origin-height=&quot;904&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;테스트를 해보니 정합성은 지켜졌다는 것을 알 수 있었었습니다. 7961 + 2039 = 10000&lt;br /&gt;이전 결과를 가져와서 비교해 봅시다.&lt;br /&gt;k6 스크립트 문제가 있었네요. finalStock이 몇 개인지 알려주지는 않네요.&lt;br /&gt;하지만 이상하게도 생각보다 효과적이지 않았습니다. 이전 결과를 불러와서 비교해 봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRD8iN/dJMcafeLFls/GJRh4Jhldff4ueG0HLtuX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRD8iN/dJMcafeLFls/GJRh4Jhldff4ueG0HLtuX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRD8iN/dJMcafeLFls/GJRh4Jhldff4ueG0HLtuX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRD8iN%2FdJMcafeLFls%2FGJRh4Jhldff4ueG0HLtuX1%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;370&quot; height=&quot;424&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;874&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;br /&gt;나아진 거라고는 duration이 조금 더 빨라졌네요.&amp;nbsp;&lt;br /&gt;한&amp;nbsp;번에&amp;nbsp;10,000&amp;nbsp;연결로&amp;nbsp;때리지&amp;nbsp;말고,&amp;nbsp;10,000명&amp;nbsp;시뮬레이션과&amp;nbsp;동시&amp;nbsp;연결&amp;nbsp;수를&amp;nbsp;분리해야&amp;nbsp;합니다.&lt;br /&gt;그러면 VUS 수를 점진적으로 올리면서 어느 지점에서 &amp;nbsp;connection refused이 발생하는지 확인해 보겠습니다.&lt;br /&gt;(200 &amp;rarr; 500 &amp;rarr; 1000 &amp;rarr; 2000)&lt;br /&gt;이것을 토대로 그래프를 그려보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;1434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZxHEY/dJMcaaYNGSs/y9cgJq3BCRhPoOkLXaQVsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZxHEY/dJMcaaYNGSs/y9cgJq3BCRhPoOkLXaQVsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZxHEY/dJMcaaYNGSs/y9cgJq3BCRhPoOkLXaQVsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZxHEY%2FdJMcaaYNGSs%2Fy9cgJq3BCRhPoOkLXaQVsk%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;615&quot; height=&quot;680&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;1434&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;예상과는 다른 결과가 나와 당황스럽네요. 이러면 DB 쪽도 확인을 해봐야 하는 생각이 문득 드는데요.&lt;br /&gt;사실 잘 모르겠습니다. 성능이 얼마나 올라갈지는 모르겠네요. ㅜㅜ&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;1456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzKHHP/dJMcaf6Srn5/rvj2d22knfKGrpI9hcgfDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzKHHP/dJMcaf6Srn5/rvj2d22knfKGrpI9hcgfDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzKHHP/dJMcaf6Srn5/rvj2d22knfKGrpI9hcgfDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzKHHP%2FdJMcaf6Srn5%2Frvj2d22knfKGrpI9hcgfDK%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;670&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;1456&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;DB-only는 해당 구간에서 측정된 p95 수치가 더 낮았지만, 동시에 실패율이 매우 높아 성능 우위로 해석하기 어렵습니다. 높은 실패율로 인해 요청이 조기 종료되면서 응답시간 지표가 낮게 관측되었을 가능성이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이거는 제가 원하는 결과가 아닙니다. 어떻게 하면 조금 더 실패율을 줄일 수 있을까요?&lt;/p&gt;
&lt;figure id=&quot;og_1774413057395&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;EDA에도 패턴들이 이렇게나 많이 존재 하다니..&quot; data-og-description=&quot;EDA(Event Driven Architecture)가 MQ나 이벤트 리스너를 통해 동작한다는 것은 알고 있었습니다. 하지만 &amp;quot;어느 상황에서 사용해야 하는가?&amp;quot;에 대해서는 명확하게 정리되어 있지 않았습니다. 단순히 도메&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/533&quot; data-og-url=&quot;https://b-programmer.tistory.com/533&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bs0nhl/dJMb82MCGrn/egxeCKjFI0s06KRGCI3Wp0/img.png?width=800&amp;amp;height=468&amp;amp;face=0_0_800_468,https://scrap.kakaocdn.net/dn/co3YGx/dJMb81fScub/7JvhKAvjMutF0PRvAaOZw0/img.png?width=800&amp;amp;height=468&amp;amp;face=0_0_800_468,https://scrap.kakaocdn.net/dn/Hshb7/dJMb84XYkl7/fO5yHYj6uN8eP9Kjl21Lpk/img.png?width=1782&amp;amp;height=1044&amp;amp;face=0_0_1782_1044&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/533&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/533&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bs0nhl/dJMb82MCGrn/egxeCKjFI0s06KRGCI3Wp0/img.png?width=800&amp;amp;height=468&amp;amp;face=0_0_800_468,https://scrap.kakaocdn.net/dn/co3YGx/dJMb81fScub/7JvhKAvjMutF0PRvAaOZw0/img.png?width=800&amp;amp;height=468&amp;amp;face=0_0_800_468,https://scrap.kakaocdn.net/dn/Hshb7/dJMb84XYkl7/fO5yHYj6uN8eP9Kjl21Lpk/img.png?width=1782&amp;amp;height=1044&amp;amp;face=0_0_1782_1044');&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;EDA에도 패턴들이 이렇게나 많이 존재 하다니..&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;EDA(Event Driven Architecture)가 MQ나 이벤트 리스너를 통해 동작한다는 것은 알고 있었습니다. 하지만 &quot;어느 상황에서 사용해야 하는가?&quot;에 대해서는 명확하게 정리되어 있지 않았습니다. 단순히 도메&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전에 EDA패턴에 대해 학습을 한 적이 있습니다. 물론 패턴에 대해 자세하게 학습은 안 했지만 말이죠.ㅎㅎ&lt;br /&gt;아무튼, 저희가 생각해야 하는 부분은 10000UV를 버틸 수 있게 만들 수 있을까입니다.&lt;br /&gt;여기서 저희가 생각할 수 있는 부분은 두 가지입니다. 처리량과 정합성을 지켜야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처리량을 지켜야 하는 이유는 몇 명이 동시에 메시지(API) 발송을 할지 모르기 때문이고&lt;br /&gt;정합성을 지켜야 하는 이유는 쿠폰을 신청을 했다면, 반드시 받아야 하기 때문입니다.&lt;br /&gt;(kafka를 사용하지 않고 스프링 이벤트로 진행해 보겠습니다. kafka는 추후에 연결시켜 보고 비교해 보겠습니다.)&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;처리량을 먼저 해결해 봅시다.&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처리량 문제를 해결하기 위해서는 작업을 어떻게 분산하고, 어떻게 동시에 처리할 것인지가 중요합니다.&lt;br /&gt;이를 위한 대표적인 패턴으로 Competing Consumer Pattern과 Asynchronous Task Execution Pattern이 있습니다.&lt;br /&gt;&lt;br /&gt;Competing Consumer Pattern은 여러 Consumer가 하나의 작업 흐름을 나누어 병렬로 처리함으로써 처리량을 높이는 방식입니다.&lt;br /&gt;반면 Asynchronous Task Execution Pattern은 작업을 비동기로 위임하여, 요청 처리와 실제 작업 수행을 분리하는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 두 가지 중 하나를 선택해서 사용하는 것이 아닌 둘 다 사용해도 무방합니다.&lt;br /&gt;위 실험결과에 따르면 200 UVS의 동시사용자에서는 예외가 발생하지 않았음을 확인하였습니다.&lt;br /&gt;그렇다면 메시지를 수신하는 Consumer를 50개 정도 만들면 될 겁니다. 하지만 Consumer의 개수를 늘린다고 해서 '메시지 처리 성능'이 좋아질 거 같지는 않습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;만약, 200에서 500 아니 1000까지 증가시킬 수 있다면 병렬적으로 사용되는 Consumer의 개수는 현저히 줄게 될 것입니다.&lt;br /&gt;500은 20개의 Consumer가 1000은 10개의 Conumser를 사용할 수 있게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 우리가 가장 먼저 해야 할 일은 메시지를 얼마나 많이 보낼 수 있는지 알아야 합니다.&lt;br /&gt;메시지를 많이 보내면 보낼수록 사용해야 할 Consumer의 개수는 현저히 줄 거라고 여겨집니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Asynchronous Task Execution Pattern&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EjY7H/dJMcagkqXeU/hSc3OuPd0NzGQEispp596k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EjY7H/dJMcagkqXeU/hSc3OuPd0NzGQEispp596k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EjY7H/dJMcagkqXeU/hSc3OuPd0NzGQEispp596k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEjY7H%2FdJMcagkqXeU%2FhSc3OuPd0NzGQEispp596k%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;390&quot; height=&quot;246&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;702&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;b&gt;1) 스프링 이벤트로 보내기&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이벤트는 두 가지 속성이 존재합니다. producer와 consumer가 존재합니다.&lt;br /&gt;publiser를 통해 이벤트를 발송하고, consumer를 통해 이벤트를 수신하는 역할을 합니다.&lt;br /&gt;자세한 내용은 아래 블로그 글을 참고해 주시기 바랍니다.&lt;/p&gt;
&lt;figure id=&quot;og_1774413036479&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;어째서 메시지 브로커가 왜 필요한가?&quot; data-og-description=&quot;카프카나 레빗 MQ같은 MQ시스템들을 학습하다보면 브로커라는 개념이 등장합니다. 인터넷에 브로커를 검색해보니 판매자와 구매자 사이에서 정보를 교환하고 중개하여 수수료를 받는 개인 또는&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/529&quot; data-og-url=&quot;https://b-programmer.tistory.com/529&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2Fpcv/dJMb81GWOQJ/X8UtnQXShlnQ8cHphuFSl1/img.png?width=800&amp;amp;height=646&amp;amp;face=0_0_800_646,https://scrap.kakaocdn.net/dn/b2lwHw/dJMb87f53uc/lhIdCIZf0AN8MJ10v5TqWk/img.png?width=800&amp;amp;height=646&amp;amp;face=0_0_800_646,https://scrap.kakaocdn.net/dn/A4ywT/dJMb87f53ub/ArySU6GMs6lAzLu8g7WtU0/img.png?width=1190&amp;amp;height=800&amp;amp;face=0_0_1190_800&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/529&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/529&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2Fpcv/dJMb81GWOQJ/X8UtnQXShlnQ8cHphuFSl1/img.png?width=800&amp;amp;height=646&amp;amp;face=0_0_800_646,https://scrap.kakaocdn.net/dn/b2lwHw/dJMb87f53uc/lhIdCIZf0AN8MJ10v5TqWk/img.png?width=800&amp;amp;height=646&amp;amp;face=0_0_800_646,https://scrap.kakaocdn.net/dn/A4ywT/dJMb87f53ub/ArySU6GMs6lAzLu8g7WtU0/img.png?width=1190&amp;amp;height=800&amp;amp;face=0_0_1190_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;어째서 메시지 브로커가 왜 필요한가?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카프카나 레빗 MQ같은 MQ시스템들을 학습하다보면 브로커라는 개념이 등장합니다. 인터넷에 브로커를 검색해보니 판매자와 구매자 사이에서 정보를 교환하고 중개하여 수수료를 받는 개인 또는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링 이벤트에서는 producer를 publisher로 consumer는 listener로 표기합니다. 이렇게 표기하는 이유는 Pub/Sub 개념을 따르기 때문이라고 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 publisher를 준비해 둡니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biHmKO/dJMcabpWsFY/3r5EqtYSQX0pmYELQBbDp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biHmKO/dJMcabpWsFY/3r5EqtYSQX0pmYELQBbDp0/img.png&quot; data-alt=&quot;publisher&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biHmKO/dJMcabpWsFY/3r5EqtYSQX0pmYELQBbDp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiHmKO%2FdJMcabpWsFY%2F3r5EqtYSQX0pmYELQBbDp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;609&quot; height=&quot;283&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;publisher&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링 이벤트는 이벤트 객체 타입(dto)에 따라 어떤 listener로 실행이 될지 결정됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dCbv3z/dJMcaiigzpP/gQsjTxUTxgRJJpF7DIFtg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dCbv3z/dJMcaiigzpP/gQsjTxUTxgRJJpF7DIFtg1/img.png&quot; data-alt=&quot;listener&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dCbv3z/dJMcaiigzpP/gQsjTxUTxgRJJpF7DIFtg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdCbv3z%2FdJMcaiigzpP%2FgQsjTxUTxgRJJpF7DIFtg1%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;576&quot; height=&quot;359&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;980&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;listener&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 비동기 이벤트로 확인하려는 목적이기 때문에 @Async를 붙여줍니다.&lt;br /&gt;하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Async&lt;/span&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;546&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6lebW/dJMcafFRg0Y/33GYicNRvbnfKqmqYA3dN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6lebW/dJMcafFRg0Y/33GYicNRvbnfKqmqYA3dN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6lebW/dJMcafFRg0Y/33GYicNRvbnfKqmqYA3dN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6lebW%2FdJMcafFRg0Y%2F33GYicNRvbnfKqmqYA3dN1%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;426&quot; height=&quot;170&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;218&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 style=&quot;background-color: #dddddd;&quot;&gt;@EnableAsync&lt;/span&gt;로 비동기를 활성화시켜야 비동기를 사용할 수 있습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;간단하게 500UV에서 실패율을 확인했기 때문에 요걸로 돌려봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;872&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bk8NTa/dJMcahcz9p0/nid20pVRN7wGfEn8FmsV5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bk8NTa/dJMcahcz9p0/nid20pVRN7wGfEn8FmsV5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bk8NTa/dJMcahcz9p0/nid20pVRN7wGfEn8FmsV5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbk8NTa%2FdJMcahcz9p0%2Fnid20pVRN7wGfEn8FmsV5K%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;359&quot; height=&quot;387&quot; data-origin-width=&quot;872&quot; data-origin-height=&quot;940&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;br /&gt;하지만 처리 속도가 월등히 높아졌다는 것을 알 수가 있었습니다.&lt;br /&gt;그렇다면 200 UVS -&amp;gt; 10000 UVS까지 결과는 어떨까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;994&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bo247/dJMb99MmUWn/Yxio2bAP0QFj1EP86xu0g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bo247/dJMb99MmUWn/Yxio2bAP0QFj1EP86xu0g1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bo247/dJMb99MmUWn/Yxio2bAP0QFj1EP86xu0g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBo247%2FdJMb99MmUWn%2FYxio2bAP0QFj1EP86xu0g1%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;687&quot; height=&quot;620&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;994&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;redis만 사용하는 방식과 비동기 이벤트 방식을 비교해 보았습니다.&lt;br /&gt;생각보다 꽤 흥미로운 결과가 나왔습니다.&lt;br /&gt;특히 1,000 VUS에서 2,000 VUS로 증가하는 구간에서는 오히려 실패율이 상승하는 모습을 확인할 수 있었습니다.&lt;br /&gt;또한 10,000 VUS 구간에서는 비동기 이벤트 방식의 실패율이 동기 방식보다 더 높게 나타났습니다.&lt;br /&gt;그렇다면 스프링 이벤트를 kafka로 변경하면 어떻게 될까요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) kafka로 보내기&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;kafka를 선택한 이유: Kafka를 선택한 이유는 단순히 요청을 비동기로 분리하기 위해서가 아닙니다.&lt;br /&gt;Kafka는 메시지를 로그 형태로 저장하고, 이를 소비자에게 제공하는 구조를 가지고 있습니다.&lt;br /&gt;이러한 특성 덕분에 장애가 발생하더라도 메시지를 다시 읽어 재처리하거나 재시도할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 Kafka를 도입한 이유는 비동기 처리 자체보다도,&lt;br /&gt;메시지의 유실 가능성을 줄이고 안정적으로 다시 처리할 수 있는 구조를 만들기 위해서입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;590&quot; data-end=&quot;620&quot; data-is-last-node=&quot;&quot; data-is-only-node=&quot;&quot;&gt;자세한 내용은 아래 블로그 글을 참고해 주시기 바랍니다.&lt;/p&gt;
&lt;figure id=&quot;og_1774413021347&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;카프카를 왜 사용해야 할까?&quot; data-og-description=&quot;카프카를 계속 공부해 왔지만, 솔직히 말하면 &amp;quot;왜 이걸 써야 하는지&amp;quot; 스스로 완전히 납득했다고 말하기는 어렵습니다.문서와 강의에서는 늘 대규모 트래픽, 이벤트 스트리밍, 비동기 아키텍처 &quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/510&quot; data-og-url=&quot;https://b-programmer.tistory.com/510&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/w7X4W/dJMb89ydaxK/0WGKXBNjKku0Y7Bia9s06K/img.png?width=800&amp;amp;height=560&amp;amp;face=0_0_800_560,https://scrap.kakaocdn.net/dn/ioBFy/dJMb86O1nB2/SPHRcyLStToTkfvdC8JLo1/img.png?width=800&amp;amp;height=560&amp;amp;face=0_0_800_560,https://scrap.kakaocdn.net/dn/cP39Jc/dJMb88F4t7v/ykN79vm02kOViMqcfmWad0/img.png?width=1502&amp;amp;height=1052&amp;amp;face=0_0_1502_1052&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/510&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/510&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/w7X4W/dJMb89ydaxK/0WGKXBNjKku0Y7Bia9s06K/img.png?width=800&amp;amp;height=560&amp;amp;face=0_0_800_560,https://scrap.kakaocdn.net/dn/ioBFy/dJMb86O1nB2/SPHRcyLStToTkfvdC8JLo1/img.png?width=800&amp;amp;height=560&amp;amp;face=0_0_800_560,https://scrap.kakaocdn.net/dn/cP39Jc/dJMb88F4t7v/ykN79vm02kOViMqcfmWad0/img.png?width=1502&amp;amp;height=1052&amp;amp;face=0_0_1502_1052');&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;카프카를 왜 사용해야 할까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카프카를 계속 공부해 왔지만, 솔직히 말하면 &quot;왜 이걸 써야 하는지&quot; 스스로 완전히 납득했다고 말하기는 어렵습니다.문서와 강의에서는 늘 대규모 트래픽, 이벤트 스트리밍, 비동기 아키텍처&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;590&quot; data-end=&quot;620&quot; data-is-last-node=&quot;&quot; data-is-only-node=&quot;&quot;&gt;이 특성은 추후 다시 다뤄질 예정입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;kafaka 또한 publisher와 listener가 필요합니다. kafka에서는 producer와 consumer라고 합니다.&lt;br /&gt;producer가 메시지를 전달하고, consumer가 메시지를 수신하게 됩니다.&lt;br /&gt;스프링 이벤트와 다른 차이점(mq에서)은 바로 broker가 존재한다는 점입니다. 즉, producer가 consumer로 바로 메시지를 전달하지 않고 broker를 통해, 메시지를 발송하고 수신하게 됩니다. 이것을 간단하게 그림을 그려보면 이렇게 그릴 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chHhUt/dJMcach4ckF/wvuo1eyeOICkijgX63ogj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chHhUt/dJMcach4ckF/wvuo1eyeOICkijgX63ogj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chHhUt/dJMcach4ckF/wvuo1eyeOICkijgX63ogj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchHhUt%2FdJMcach4ckF%2Fwvuo1eyeOICkijgX63ogj1%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;1478&quot; height=&quot;210&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div data-message-author-role=&quot;assistant&quot; data-message-id=&quot;2d035d44-c19c-4641-aa40-92360a007c3b&quot; data-message-model-slug=&quot;gpt-5-3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;35&quot; data-end=&quot;165&quot;&gt;broker를 사용하는 가장 큰 이점은 메시지를 발송하는 쪽과 메시지를 처리하는 쪽이 분리된다는 점입니다.&lt;br /&gt;Producer는 메시지를 보내는 역할만 담당하고, Consumer는 메시지를 받아 처리하는 역할을 담당합니다.&lt;br /&gt;이처럼 메시지 발행과 처리 과정이 분리되면 시스템 간 결합도가 낮아지고, 처리 구조를 유연하게 확장할 수 있습니다.&lt;br /&gt;해당 내용은 다음 글에서 더 자세하게 설명하겠습니다. 자세한 내용은 다음 글을 참고해 주세요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;figure id=&quot;og_1774412991652&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;어째서 메시지 브로커가 왜 필요한가?&quot; data-og-description=&quot;카프카나 레빗 MQ같은 MQ시스템들을 학습하다보면 브로커라는 개념이 등장합니다. 인터넷에 브로커를 검색해보니 판매자와 구매자 사이에서 정보를 교환하고 중개하여 수수료를 받는 개인 또는&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/529&quot; data-og-url=&quot;https://b-programmer.tistory.com/529&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2Fpcv/dJMb81GWOQJ/X8UtnQXShlnQ8cHphuFSl1/img.png?width=800&amp;amp;height=646&amp;amp;face=0_0_800_646,https://scrap.kakaocdn.net/dn/b2lwHw/dJMb87f53uc/lhIdCIZf0AN8MJ10v5TqWk/img.png?width=800&amp;amp;height=646&amp;amp;face=0_0_800_646,https://scrap.kakaocdn.net/dn/A4ywT/dJMb87f53ub/ArySU6GMs6lAzLu8g7WtU0/img.png?width=1190&amp;amp;height=800&amp;amp;face=0_0_1190_800&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/529&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/529&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2Fpcv/dJMb81GWOQJ/X8UtnQXShlnQ8cHphuFSl1/img.png?width=800&amp;amp;height=646&amp;amp;face=0_0_800_646,https://scrap.kakaocdn.net/dn/b2lwHw/dJMb87f53uc/lhIdCIZf0AN8MJ10v5TqWk/img.png?width=800&amp;amp;height=646&amp;amp;face=0_0_800_646,https://scrap.kakaocdn.net/dn/A4ywT/dJMb87f53ub/ArySU6GMs6lAzLu8g7WtU0/img.png?width=1190&amp;amp;height=800&amp;amp;face=0_0_1190_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;어째서 메시지 브로커가 왜 필요한가?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카프카나 레빗 MQ같은 MQ시스템들을 학습하다보면 브로커라는 개념이 등장합니다. 인터넷에 브로커를 검색해보니 판매자와 구매자 사이에서 정보를 교환하고 중개하여 수수료를 받는 개인 또는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;broker는 직접적으로 구현하지는 않습니다. 즉, producer와 consumer만 구현하면 됩니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;producer를 만들어줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1584&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EPPC5/dJMcadVxEOn/2b6kgTqbweifd9kJ2UMxI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EPPC5/dJMcadVxEOn/2b6kgTqbweifd9kJ2UMxI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EPPC5/dJMcadVxEOn/2b6kgTqbweifd9kJ2UMxI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEPPC5%2FdJMcadVxEOn%2F2b6kgTqbweifd9kJ2UMxI1%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;696&quot; height=&quot;250&quot; data-origin-width=&quot;1584&quot; data-origin-height=&quot;570&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;기존 스프링 이벤트가 이벤트 DTO로 통신을 했다면, kafka는 토픽으로 통신을 하게 됩니다. 즉, coupon-issue-requests라는 토픽으로 메시지를 전송하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;producer를 만들었으니 이것을 수신하는 consumer도 만들어 봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tOqfZ/dJMcafy7swZ/LfJZbMtaflRikHO9tzvNZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tOqfZ/dJMcafy7swZ/LfJZbMtaflRikHO9tzvNZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tOqfZ/dJMcafy7swZ/LfJZbMtaflRikHO9tzvNZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtOqfZ%2FdJMcafy7swZ%2FLfJZbMtaflRikHO9tzvNZ1%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;635&quot; height=&quot;228&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;586&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;이 코드는 producer로 발행된 토픽을 받아오는 역할을 합니다. 이렇게 되면 카프카를 통해 다음과 같이 전달이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2454&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwGept/dJMcaaq2d03/fk856Uk8waYhCKjrEPglR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwGept/dJMcaaq2d03/fk856Uk8waYhCKjrEPglR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwGept/dJMcaaq2d03/fk856Uk8waYhCKjrEPglR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwGept%2FdJMcaaq2d03%2Ffk856Uk8waYhCKjrEPglR0%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;624&quot; height=&quot;201&quot; data-origin-width=&quot;2454&quot; data-origin-height=&quot;790&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;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이번에도 역시 500UV에서 한번 돌려보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;1102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sd4Or/dJMcafZ9nMi/pk3bpzQbIVEkopWVKCpLCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sd4Or/dJMcafZ9nMi/pk3bpzQbIVEkopWVKCpLCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sd4Or/dJMcafZ9nMi/pk3bpzQbIVEkopWVKCpLCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsd4Or%2FdJMcafZ9nMi%2Fpk3bpzQbIVEkopWVKCpLCk%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;474&quot; height=&quot;646&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;1102&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;스프링이벤트보다 실패율이나 속도가 많이 증가가 된 걸로 확인이 됩니다. 물론, 이거 하나로 판단하기에는 어려움이 있다고 여겨집니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 200UV부터 10000UV까지 돌린 결과는 어떨까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;1372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dXWThD/dJMcacJap77/HOCLSKzf9fQz1ZzsP7KHo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dXWThD/dJMcacJap77/HOCLSKzf9fQz1ZzsP7KHo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dXWThD/dJMcacJap77/HOCLSKzf9fQz1ZzsP7KHo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdXWThD%2FdJMcacJap77%2FHOCLSKzf9fQz1ZzsP7KHo1%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;728&quot; height=&quot;597&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;1372&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;생각보다 성능이 월등하게 좋아졌다고 말하기는 애매한 결과입니다. 전체적으로 보면 kafka 방식의 실패율이 더 낮은 경향을 보이긴 했지만, 5000 VUS에서는 오히려 kafka 방식의 실패율이 더 높게 나타났습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;187&quot; data-end=&quot;285&quot;&gt;이러한 현상이 발생한 이유는 kafka 이전 단계에서 오류가 발생했을 가능성이 높기 때문입니다.&lt;br /&gt;즉, 요청이 kafka까지 전달되기 전에 이미 실패했을 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;287&quot; data-end=&quot;306&quot;&gt;예를 들면 다음과 같은 경우입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;308&quot; data-end=&quot;389&quot;&gt;
&lt;li data-section-id=&quot;1moqf6d&quot; data-start=&quot;308&quot; data-end=&quot;332&quot;&gt;HTTP 요청 처리 과정에서 발생한 예외&lt;/li&gt;
&lt;li data-section-id=&quot;1lpkhzs&quot; data-start=&quot;333&quot; data-end=&quot;350&quot;&gt;애플리케이션 내부 처리 오류&lt;/li&gt;
&lt;li data-section-id=&quot;a5oxek&quot; data-start=&quot;351&quot; data-end=&quot;371&quot;&gt;로컬 네트워크 지연이나 연결 문제&lt;/li&gt;
&lt;li data-section-id=&quot;32v2n8&quot; data-start=&quot;372&quot; data-end=&quot;389&quot;&gt;서버의 일시적인 리소스 부족&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;391&quot; data-end=&quot;476&quot;&gt;이러한 오류들은 kafka로 메시지가 전송되기 이전 단계에서 발생하기 때문에, kafka 자체의 처리 방식과는 직접적인 관련이 없을 수 있습니다. 따라서 일부 구간에서 kafka 방식의 실패율이 더 높게 나타난 것은 kafka의 문제라기보다는 요청이 kafka에 도달하기 이전 단계에서 발생한 오류일 가능성이 높다고 볼 수 있습니다. 이 말을 다시 말하면, 환경에 따라 결과는 다르게 나올 수 있다는 뜻이 됩니다.&lt;br /&gt;이것을 kafka를 사용해서 해결할 수는 없습니다. 대신 이러한 실패에 대해서는, 실패 내역을 남기고 이를 바탕으로 재발행하거나 재처리하는 방식으로 실패율을 낮출 수 있습니다. 이러한 문제를 해결하기 위한 대표적인 방법 중 하나가 바로 Outbox Pattern입니다.&amp;nbsp;&lt;br /&gt;이 방식은 추후 다뤄보겠습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-start=&quot;391&quot; data-end=&quot;476&quot;&gt;Competing Consumer Pattern&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;391&quot; data-end=&quot;476&quot;&gt;다시 말하지만, 이 패턴을 사용한다고 해서 실패율을 0으로 만들 수 있는 것은 아닙니다.&lt;br /&gt;다만 여러 Consumer가 동시에 메시지를 처리할 수 있기 때문에 전체 처리 속도와 처리량을 향상하는 효과는 기대할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;185&quot; data-end=&quot;321&quot;&gt;그럼에도 불구하고 Outbox Pattern보다 먼저 다루는 이유는, 이 패턴이 Asynchronous Task Execution Pattern과 마찬가지로 처리량(throughput)을 개선하기 위한 접근 방식에 속하기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;323&quot; data-end=&quot;456&quot; data-is-last-node=&quot;&quot; data-is-only-node=&quot;&quot;&gt;즉, Competing Consumer Pattern은 메시지의 신뢰성이나 재처리를 해결하기 위한 패턴이라기보다는,&lt;br /&gt;여러 Consumer가 병렬로 작업을 처리하여 시스템의 처리량을 높이기 위한 패턴이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;병렬로 작업하기 위해서는 어떻게 해야 할까요?&lt;br /&gt;이것을 이해하기 위해서는 kafka-key를 이해를 해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 couponId로 키를 나눴습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YQgH4/dJMcafy7BcV/MaSHCOgPKGAvkmx0qY8xHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YQgH4/dJMcafy7BcV/MaSHCOgPKGAvkmx0qY8xHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YQgH4/dJMcafy7BcV/MaSHCOgPKGAvkmx0qY8xHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYQgH4%2FdJMcafy7BcV%2FMaSHCOgPKGAvkmx0qY8xHK%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;461&quot; height=&quot;570&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1236&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;br /&gt;그렇다면, 현재 방식에서 최대한 키를 겹치지 않게 하는 방법은 userId 또는 requestId입니다.&lt;br /&gt;그렇게 되면 다음과 같이 그릴 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nNiDE/dJMcacCoi2J/Rt1kJxKkuE2Sben2OvLrEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nNiDE/dJMcacCoi2J/Rt1kJxKkuE2Sben2OvLrEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nNiDE/dJMcacCoi2J/Rt1kJxKkuE2Sben2OvLrEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnNiDE%2FdJMcacCoi2J%2FRt1kJxKkuE2Sben2OvLrEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;546&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1236&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;이제 파티션 0번은 101번만 파티션 1번은 102번만, 파티션 2는 103번만 작업할 수 있게 되었습니다. 이렇게 되면 병렬적으로 작업할 수 있어 이전 테스트 asychronous를 할 때보다 작업효율이 높아질 거라고 예상합니다.&lt;br /&gt;그럼에도 불구하고, 속도가 빠르냐는 다른 문제라고 여겨집니다. 왜냐하면 작업 효율이 좋다고 해서 무턱대고 속도가 빠르다고 말하는 건 결론부터 말하는 꼴이기 때문에 추후 테스트를 통해 설명드려보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7DQKv/dJMcaiJj7xS/DzycQoT7p1XBbdTyLYJvOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7DQKv/dJMcaiJj7xS/DzycQoT7p1XBbdTyLYJvOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7DQKv/dJMcaiJj7xS/DzycQoT7p1XBbdTyLYJvOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7DQKv%2FdJMcaiJj7xS%2FDzycQoT7p1XBbdTyLYJvOk%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;737&quot; height=&quot;270&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;594&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;producer를 couponId에서 userId로 변경하였습니다. producer 쪽은 많은 파티션을 사용하기 위해 key만 변경하였습니다.&lt;br /&gt;그렇다면, consumer는 코드가 어떻게 변경되었을까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tkw30/dJMcac3qeQA/rHMgqpkXDPB8NvAF2KHrv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tkw30/dJMcac3qeQA/rHMgqpkXDPB8NvAF2KHrv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tkw30/dJMcac3qeQA/rHMgqpkXDPB8NvAF2KHrv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftkw30%2FdJMcac3qeQA%2FrHMgqpkXDPB8NvAF2KHrv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;756&quot; height=&quot;298&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;656&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;consumer 쪽을 확인해 보니 용어가 두 가지가 등장하였습니다. groupId와 concurrency입니다.&lt;br /&gt;이들은 무엇이며 concurrency에 적힌 숫자는 의미가 무엇일까요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;groupId&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;groupId는 현 프로젝트에서는 엄청나게 의미가 없을 수도 있습니다. 왜냐하면 컨슈머 여러 개를 하나의 팀으로 만드는 건데&lt;br /&gt;현 프로젝트에서는 group이라고 해봤자 요거 하나뿐이기 때문이죠.&amp;nbsp;&lt;br /&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;1574&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZ8M7S/dJMb99ZZedG/w84L73l8fEEbvozhgWgDV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZ8M7S/dJMb99ZZedG/w84L73l8fEEbvozhgWgDV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZ8M7S/dJMb99ZZedG/w84L73l8fEEbvozhgWgDV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZ8M7S%2FdJMb99ZZedG%2Fw84L73l8fEEbvozhgWgDV1%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;767&quot; height=&quot;363&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;744&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;size18&quot;&gt;&lt;b&gt;concurrency&lt;/b&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;concurrency&lt;/span&gt;는 동시에 병렬로 동작할 수 있는 consumer의 개수를 나눠서 처리를 할 수 있습니다.&lt;br /&gt;그렇다면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;concurrency&lt;/span&gt;는 많으면 많을수록 좋을까요? 만약, 입구의 크기가 최대 500 UVS라면 10번을 진행하게 되면 5000이고 20번을 하게 되면 10000번이 훌쩍 넘어가게 됩니다.&amp;nbsp;&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: #dddddd;&quot;&gt;concurrency&lt;/span&gt;를 무턱 되고 올리면 다음과 같은 상황이 발생하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 파티션 개수를 적게 생성하게 된다면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;concurrency&lt;/span&gt;를 높게 선정하는 것이 의미가 없어집니다. 즉, 파티션이 3이고 concurrency이 10개라면, 동시에 작업하는 consumer는 총 3개입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 만약, 파티션 개수는 상관없다고 한다고 해도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;concurrency&lt;/span&gt;를 높게 설정하는 것은 다음과 같은 문제를 초래합니다.&lt;br /&gt;동시에 많은 consumer가 동작한다는 것은 스레드를 많이 사용한다는 뜻이니, 콘텍스트 스위칭이 많이 발생하게 됩니다.&lt;br /&gt;즉, 콘텍스트 스위칭 비용이 증가된다는 뜻이 되는 거죠.&lt;br /&gt;이 밖에도 DB 커넥션 경쟁, Redis / 외부 API 부하 증가, 락 경합 증가, 중복 처리 / race condition 가능성 증가, 로그/모니터링 복잡도 증가 등등..&amp;nbsp; 그러니까 처리량보다 충돌이 먼저 발생이 될 수 있다는 뜻이 됩니다. 아이러니하게도 성능이 떨어지는 꼴이 됩니다.&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;1438&quot; data-origin-height=&quot;982&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x3SDq/dJMcahDGapO/FULhJsIFDjkvfGxj7G04G1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x3SDq/dJMcahDGapO/FULhJsIFDjkvfGxj7G04G1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x3SDq/dJMcahDGapO/FULhJsIFDjkvfGxj7G04G1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx3SDq%2FdJMcahDGapO%2FFULhJsIFDjkvfGxj7G04G1%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;527&quot; height=&quot;360&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;982&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: #dddddd;&quot;&gt;concurrency&lt;/span&gt;는 시스템이 버틸 수 있는 최대 개수까지 적당히 높이는 것이 핵심이라고 여겨집니다.&amp;nbsp;&lt;br /&gt;또, 현재 테스트하고 있는 환경이 문제가 로컬이라는 점입니다. 결국, 로컬환경에서의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;concurrency&lt;/span&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&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;concurrency&lt;/span&gt;를 높게 설정하면 안 되는지 테스트를 진행해 보겠습니다. 이렇게 만들면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;competing Consumer patten&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;size18&quot;&gt;&lt;b&gt;1. 중복 처리와 멱등성 처리&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 요청이 들어와도 같은 유저에게는 쿠폰이 2번 이상 발급이 되지 않게 해야 합니다.&lt;br /&gt;현재는 다음과 같은 코드가 존재합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FPsd8/dJMcahjqP0Y/kfUFKCmnwiE561z6H3Onz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FPsd8/dJMcahjqP0Y/kfUFKCmnwiE561z6H3Onz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FPsd8/dJMcahjqP0Y/kfUFKCmnwiE561z6H3Onz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFPsd8%2FdJMcahjqP0Y%2FkfUFKCmnwiE561z6H3Onz0%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;1504&quot; height=&quot;170&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;170&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;br /&gt;하지만 이것만으로는 부족합니다.&amp;nbsp; DB에서 최종적으로 중복 저장을 막아야 합니다.&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;772&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0D1ut/dJMcai3Cz5J/tMshcFkmFyhZcjGwfanMDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0D1ut/dJMcai3Cz5J/tMshcFkmFyhZcjGwfanMDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0D1ut/dJMcai3Cz5J/tMshcFkmFyhZcjGwfanMDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0D1ut%2FdJMcai3Cz5J%2FtMshcFkmFyhZcjGwfanMDK%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;548&quot; height=&quot;220&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;310&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;왜냐하면, 동시에 두 요청이 들어와서 race condition이 발생한다면, 최종적으로 DB가 막아 줄 수 있기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 지금 방식은 최종 결과의 종복방지지 이벤트 자체의 멱등성과는 거리가 있어 보입니다.&lt;br /&gt;그렇다면, 어떻게 처리할 수 있을까요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 이벤트에는 couponId, userId, requesedAt뿐만 존재합니다. 여기에는 이벤트의 고유한 값이 존재하지 않습니다.&lt;br /&gt;그렇다는 이야기는 이벤트에 대한 고유한 값을 적용시키지 못한다는 뜻이 됩니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;그렇다면 어떻게 해야 할까요? eventId라는 고유한 값을 추가한 뒤, 이미 처리가 되어있다면, 무시를 하고&lt;br /&gt;처음 본 이벤트 하면 proccessed_event 같은 저장소에 기록하면 됩니다.&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;producer 쪽에서 보낸 메시지를 수신한 뒤, consumer 쪽에서 수신을 하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;1064&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYQcS2/dJMcacCpgjm/A83gd0yZAMISDCKIMA4mZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYQcS2/dJMcacCpgjm/A83gd0yZAMISDCKIMA4mZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYQcS2/dJMcacCpgjm/A83gd0yZAMISDCKIMA4mZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYQcS2%2FdJMcacCpgjm%2FA83gd0yZAMISDCKIMA4mZk%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;619&quot; height=&quot;374&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;1064&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;이전과 다른 점은 eventId를 저장하고 있고 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;coponId와 userId가 코드 상에서 멱등성을 측정한 것처럼 역시&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lZYei/dJMcadutq1K/XuyvjHvFtraN2sWdhV5TNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lZYei/dJMcadutq1K/XuyvjHvFtraN2sWdhV5TNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lZYei/dJMcadutq1K/XuyvjHvFtraN2sWdhV5TNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlZYei%2FdJMcadutq1K%2FXuyvjHvFtraN2sWdhV5TNk%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;1526&quot; height=&quot;222&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;222&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;애플리케이션에서 동일한 이벤트가 다시 발행되더라도, 재고가 중복 차감되거나 쿠폰이 중복 발급되지 않도록 하기 위해서입니다. 하지만 이상한 점이 있습니다. 어째서 예외를 던지지 않고 return을 하게 되는 걸까요? 그 이유는 첫째, 이벤트의 중복 발행을 예외 상황으로 보지 않기 위함입니다. 둘째,&amp;nbsp;이미&amp;nbsp;처리된&amp;nbsp;이벤트에&amp;nbsp;대해&amp;nbsp;불필요한&amp;nbsp;재처리&amp;nbsp;없이&amp;nbsp;그대로&amp;nbsp;종료하기&amp;nbsp;위함입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처리&amp;nbsp;도중&amp;nbsp;다른&amp;nbsp;스레드가&amp;nbsp;먼저&amp;nbsp;성공하는&amp;nbsp;경우도&amp;nbsp;고려해야&amp;nbsp;합니다.&lt;br /&gt;앞선 조회는 이미 처리 완료된 이벤트를 빠르게 걸러내기 위한 사전 확인입니다.&lt;br /&gt;다만 동시 요청 상황에서는 둘 다 조회 시점에는 미처리로 판단할 수 있으므로, 저장 충돌이 발생한 뒤 다시 조회해 선행 처리 결과를 확인합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LgdZy/dJMcahjq2Gy/qGkuD7dcKR0wEq6anmdO20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LgdZy/dJMcahjq2Gy/qGkuD7dcKR0wEq6anmdO20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LgdZy/dJMcahjq2Gy/qGkuD7dcKR0wEq6anmdO20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLgdZy%2FdJMcahjq2Gy%2FqGkuD7dcKR0wEq6anmdO20%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;803&quot; height=&quot;198&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;410&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;동시에 요청이 들어온다면, 한쪽이 먼저 성공했다면, 다른 쪽은 DB unique 제약에 걸려 실패하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rH6vI/dJMcahwW8qe/k78HHkRltKVqipJWU5P3A0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rH6vI/dJMcahwW8qe/k78HHkRltKVqipJWU5P3A0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rH6vI/dJMcahwW8qe/k78HHkRltKVqipJWU5P3A0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrH6vI%2FdJMcahwW8qe%2Fk78HHkRltKVqipJWU5P3A0%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;677&quot; height=&quot;137&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;182&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;참고로&amp;nbsp;쿠폰&amp;nbsp;재고를&amp;nbsp;다시&amp;nbsp;증가시키는&amp;nbsp;이유는,&amp;nbsp;이미&amp;nbsp;Redis에서&amp;nbsp;재고를&amp;nbsp;감소시킨&amp;nbsp;이후&amp;nbsp;예외가&amp;nbsp;발생했기&amp;nbsp;때문에&amp;nbsp;차감한&amp;nbsp;재고를&amp;nbsp;복구하기&amp;nbsp;위해서입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 파트에 대한 내용은 추후 outbox를 설명할 때, 다시 등장할 예정입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 동시성 안전성&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청이 몰려서 재고보다 많이 발급되지 않게 해야 합니다.&lt;br /&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uZKIG/dJMcadONqo2/KcvvBj6AXhqBf5cFHDHv01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uZKIG/dJMcadONqo2/KcvvBj6AXhqBf5cFHDHv01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uZKIG/dJMcadONqo2/KcvvBj6AXhqBf5cFHDHv01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuZKIG%2FdJMcadONqo2%2FKcvvBj6AXhqBf5cFHDHv01%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;623&quot; height=&quot;355&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;960&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;이로써, 여러 요청이 동시에 들어와도 재고가 0보다 작아지면 다시 복구하고 실패처리가 가능합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;둘째, 같은 유저가 같은 쿠폰을 동시에 여러 번 발급받으려고 해도 최종적으로 하나만 저장이 됩니다.&lt;br /&gt;즉, 동시 요청이 들어와도 동일 유저의 중복 방지 발급은 방어할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2342&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YxOFx/dJMcaadxDKA/hXVvb0bK5z4pOxxHaKTT11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YxOFx/dJMcaadxDKA/hXVvb0bK5z4pOxxHaKTT11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YxOFx/dJMcaadxDKA/hXVvb0bK5z4pOxxHaKTT11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYxOFx%2FdJMcaadxDKA%2FhXVvb0bK5z4pOxxHaKTT11%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;822&quot; height=&quot;237&quot; data-origin-width=&quot;2342&quot; data-origin-height=&quot;674&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;&amp;nbsp;이 정도만 해도 어느 정도, 동시성은 보장이 됩니다. 조금 더 정확하게 동시성을 적용하려면 다음과 같은 것들을 적용할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재는 Redis 재고 차감과 DB 저장이 하나의 원자적 작업이 아닙니다.&amp;nbsp;&lt;br /&gt;현재는 중복을 확인하고 Redis 재고 감소가 발생하며, 그 후에 DB에 저장이 됩니다.&lt;br /&gt;하지만 이 과정이 하나의 원자적 연산으로 묶여 있지는 않습니다. 즉, 처리 중간에 실패가 나면 잠시 어긋날 가능성이 존재합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OyVCC/dJMcagLBaLR/p1SqFrWMikywXQ57bPymHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OyVCC/dJMcagLBaLR/p1SqFrWMikywXQ57bPymHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OyVCC/dJMcagLBaLR/p1SqFrWMikywXQ57bPymHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOyVCC%2FdJMcagLBaLR%2Fp1SqFrWMikywXQ57bPymHk%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;752&quot; height=&quot;349&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;552&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;Redis 재고 차감과 DB 저장은 하나의 원자적 연산이 아닙니다.&amp;nbsp;&amp;nbsp;현재는 Redis에서 재고를 먼저 감소시킨 뒤 DB에 발급 쿠폰을 저장합니다. 이때 DB 저장이 실패하면, 감소했던 Redis 재고를 다시 증가시켜 보정합니다. 즉, 정상적인 예외 흐름에서는 보상 로직이 존재하지만, Redis 차감 직후 프로세스 장애 같은 상황까지 완전히 원자적으로 보장하는 구조는 아닙니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면, 원자적으로 동작을 시키려면 어떻게 수정할 수 있을까요?&lt;br /&gt;일단 우리가 생각해야 되는 것이 DB를 기준으로 정합성이 발생을 시켜야 된다는 점입니다. 현재는 redis를 기준으로 정합성이 발생하고 있습니다. 이를 다시 말하면 보조 수단으로 사용을 해야 하는 redis가 메인으로 사용이 되었다고 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1748&quot; data-origin-height=&quot;1602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSQvuL/dJMcahX6IHZ/53S88suFmPO4L1X9dZvUT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSQvuL/dJMcahX6IHZ/53S88suFmPO4L1X9dZvUT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSQvuL/dJMcahX6IHZ/53S88suFmPO4L1X9dZvUT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSQvuL%2FdJMcahX6IHZ%2F53S88suFmPO4L1X9dZvUT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;579&quot; height=&quot;531&quot; data-origin-width=&quot;1748&quot; data-origin-height=&quot;1602&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;다시 동시성을 완벽하게 통제하기 위해 DB로 바뀐 것을 확인할 수 있습니다.&lt;br /&gt;그러면 redis는 어떻게 바뀌었을까요?&lt;br /&gt;바로 DB의 트랜잭션이 종료된 이후에 반영이 됩니다. 바로 다음과 같은 코드로 반영이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rJaVe/dJMcaaECJkd/FDqZ3Q521ETNDjKVLiOMJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rJaVe/dJMcaaECJkd/FDqZ3Q521ETNDjKVLiOMJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rJaVe/dJMcaaECJkd/FDqZ3Q521ETNDjKVLiOMJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrJaVe%2FdJMcaaECJkd%2FFDqZ3Q521ETNDjKVLiOMJK%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;734&quot; height=&quot;268&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;604&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;코드가 두 가지로 나뉘는 이유는, 트랜잭션 동기화가 활성화된 경우와 활성화되지 않은 경우를 구분하기 위해서입니다. 동기화가 활성화되어 있으면 트랜잭션이 정상 커밋된 후 Redis 캐시를 삭제하고,&amp;nbsp;활성화되어 있지 않으면 Redis 캐시를 즉시 삭제합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이전 코드에서는 redis 차감이 발생하였지만 이제는 차감이 아니라 제거하는 방식을 선택하였습니다.&lt;br /&gt;이렇게 만든 이유는 redis를 재고의 기준 저장소로 쓰지 않고, 조회 캐시로만 사용하기로 했기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 이런 의문이 들 수 있습니다. redis가 이제 의미가 없는 걸까? 현재 구조에서는 redis는 보조 수단으로 넘어갔기 때문에 조회 최적화를 위한 의미로 가진다고 볼 수 있습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1790&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKkxiy/dJMcagku45O/Xd1peqvQh0z1rmgaHJwu5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKkxiy/dJMcagku45O/Xd1peqvQh0z1rmgaHJwu5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKkxiy/dJMcagku45O/Xd1peqvQh0z1rmgaHJwu5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKkxiy%2FdJMcagku45O%2FXd1peqvQh0z1rmgaHJwu5k%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;1790&quot; height=&quot;426&quot; data-origin-width=&quot;1790&quot; data-origin-height=&quot;426&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;조회 시 redis에 데이터가 존재하지 않는다면, DB를 데이터가 존재한다면 redis를 가져오는 코드입니다.&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;500UV만 돌려보겠습니다. 200~10000 UVS는 추후 테스트 예정입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;1120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B8yO9/dJMcagEQmSb/UiankBHq9iqK8icK1geuI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B8yO9/dJMcagEQmSb/UiankBHq9iqK8icK1geuI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B8yO9/dJMcagEQmSb/UiankBHq9iqK8icK1geuI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB8yO9%2FdJMcagEQmSb%2FUiankBHq9iqK8icK1geuI0%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;575&quot; height=&quot;799&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;1120&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;걱정했던 것과 달리, 이번 테스트에서는 실패가 발생하지 않았습니다. 물론 1회 실행 결과이기 때문에 오차 가능성은 있지만, 응답 속도도 전반적으로 준수한 수준으로 보입니다. 이를&amp;nbsp;통해&amp;nbsp;재고&amp;nbsp;차감&amp;nbsp;과정의&amp;nbsp;동시성&amp;nbsp;제어가&amp;nbsp;현재&amp;nbsp;기준에서는&amp;nbsp;비교적&amp;nbsp;안정적으로&amp;nbsp;동작하고&amp;nbsp;있다고&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 consumer을 3으로 지정했습니다. consumer:3은 기존 Asynchronous Task Execution Pattern과 비교를 해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crJ4c8/dJMcag5TLjc/qsUo8TTQXG0iNmJgm8zxV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crJ4c8/dJMcag5TLjc/qsUo8TTQXG0iNmJgm8zxV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crJ4c8/dJMcag5TLjc/qsUo8TTQXG0iNmJgm8zxV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrJ4c8%2FdJMcag5TLjc%2FqsUo8TTQXG0iNmJgm8zxV1%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;658&quot; height=&quot;681&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;1114&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;500 VUS 구간에서 소폭의 실패율 증가가 관찰되었습니다. 다만 이는 RDB의 구조적 한계라기보다는, 일시적인 EOF/connection issue 또는 애플리케이션 인스턴스의 순간적인 흔들림에 기인했을 가능성이 큽니다. 그럼에도 전체적으로는 고부하 구간에서 실패율이 크게 감소해, 안정성 측면에서는 오히려 개선된 것으로 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제부터는 동시에 얼마나 많은 consumer를 활용할 수 있는지 확인해보려 합니다. 이를 위해 concurrency 값을 점진적으로 증가시키면서, 실제 처리량이 얼마나 늘어나는지와 어느 지점부터 한계가 나타나는지를 함께 살펴볼 예정입니다.&lt;br /&gt;&lt;br /&gt;concurrency를 3번부터 10번까지 증가시키면서 테스트를 진행해 보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;1084&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tmwF7/dJMcadBfTR3/Ndv5kil5ZRGHxHVtUb1Vl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tmwF7/dJMcadBfTR3/Ndv5kil5ZRGHxHVtUb1Vl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tmwF7/dJMcadBfTR3/Ndv5kil5ZRGHxHVtUb1Vl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtmwF7%2FdJMcadBfTR3%2FNdv5kil5ZRGHxHVtUb1Vl1%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;2004&quot; height=&quot;1084&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;1084&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;2012&quot; data-origin-height=&quot;1422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYbS1L/dJMcafFVlR3/oyIAxHT5Em2uVH6Jmnb7n0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYbS1L/dJMcafFVlR3/oyIAxHT5Em2uVH6Jmnb7n0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYbS1L/dJMcafFVlR3/oyIAxHT5Em2uVH6Jmnb7n0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYbS1L%2FdJMcafFVlR3%2FoyIAxHT5Em2uVH6Jmnb7n0%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;2012&quot; height=&quot;1422&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;1422&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;이제 Competing Consumer Pattern까지 적용시켜 봤습니다. 동시성을 지키기 위해서는 자원이 가진 ID들 뿐만 아니라 이벤트까지 중복을 체크를 해야 했습니다. 또한, 애플리케이션에서 중복을 체크를 하지 못하는 것을 방지하고자 DB단에서 키 관리를 하여 최종적으로 체크하여 중복을 최대한 줄였습니다.&lt;br /&gt;데이터 일관성을 지키기 위해 레디스 차감 방식이 아닌, RDB에서 차감하는 방식으로 변경하였으며, 비관락까지 재 추가하였습니다. 이는 레디스로 옮기는 과정에서 누락이 있었던 걸로 파악이 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 처리량은 마무리를 지었습니다. 제 목표는 10000 UVS까지 안전하고 빠르게 전달하는 것이 목표입니다. 고로 concurrency에서 가장 빠른 성능을 보여준 concurrency=9를 중심으로 진행될 예정이며, 만약, 속도가 너무 느리다면,&lt;br /&gt;5도 함께 진행해 볼 예정입니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Outbox 패턴 적용&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처리량 개선을 위한 패턴 적용이 마무리되었으니, 이제는 정합성과 안정성을 높이는 단계로 넘어가 보겠습니다. 단순히 요청을 빠르게 처리하는 것만으로는 충분하지 않습니다. 중요한 것은 실제로 누락 없이 완료되는 요청의 수를 늘리는 것입니다. 이러한 문제를 해결하기 위한 대표적인 방법이 Outbox 패턴입니다. 이번에는 Outbox 패턴을 적용하여, 데이터 저장과 메시지 발행 사이의 불일치 가능성을 줄이고 최종 완료 건수를 높여보겠습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Outbox 패턴이란?&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Outbox 패턴은 비즈니스 데이터 변경과 이벤트 발행을 분리하되, 둘 사이의 정합성을 보장하기 위한 패턴입니다.&lt;br /&gt;지금까지는 consumer 측에서 중복 처리와 동시성 제어를 통해 안정성을 확보했지만, producer 측에서는 DB 변경과 Kafka 발행이 하나의 원자적 흐름으로 묶여 있지 않아 이벤트 유실 가능성이 있습니다. 이를 해결하기 위해 비즈니스 데이터와 함께 outbox 이벤트를 같은 트랜잭션으로 저장하고, 이후 별도 relay/publisher가 이를 Kafka로 발행하는 방식이 outbox 패턴입니다.&lt;br /&gt;일단 간단하게&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bleCp6/dJMcabDyFLY/thZ83OSkWARzAMnGrgTEkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bleCp6/dJMcabDyFLY/thZ83OSkWARzAMnGrgTEkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bleCp6/dJMcabDyFLY/thZ83OSkWARzAMnGrgTEkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbleCp6%2FdJMcabDyFLY%2FthZ83OSkWARzAMnGrgTEkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;644&quot; height=&quot;474&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;940&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;outbox에 이벤트를 저장하고 바로 프로듀서를 통해 이벤트를 넘겨주도록 하였습니다.&lt;br /&gt;하지만 결과는...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;1108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjIGNS/dJMcaduw81I/McYVW3vWbJWvfmcZu7kxD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjIGNS/dJMcaduw81I/McYVW3vWbJWvfmcZu7kxD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjIGNS/dJMcaduw81I/McYVW3vWbJWvfmcZu7kxD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjIGNS%2FdJMcaduw81I%2FMcYVW3vWbJWvfmcZu7kxD0%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;470&quot; height=&quot;473&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;1108&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;흐음... kafka로 통신으로 9937개가 성공하였습니다. 여가까지는 뭐 그럴 수 있다고 생각이 듭니다. 왜냐하면 저에게는 이제 outbox가 존재하기 때문이죠. 하지만..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bN2j47/dJMcahqeQR1/SpkGkApAfPirYEn5qMFuk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bN2j47/dJMcahqeQR1/SpkGkApAfPirYEn5qMFuk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bN2j47/dJMcahqeQR1/SpkGkApAfPirYEn5qMFuk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbN2j47%2FdJMcahqeQR1%2FSpkGkApAfPirYEn5qMFuk1%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;553&quot; height=&quot;154&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;202&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;9937개가 저장이 되었습니다. concurrency도 증가시켜 봤고, VUS를 증가시켜 봤지만 결과는 똑같았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사실 정합성이라는 것은 단순히 모든 요청이 성공해야 한다는 의미는 아닙니다. 성공한 요청 수와 실패한 요청 수를 합친 결과가 전체 입력값과 일치하고, 그 결과가 시스템의 기대 동작과 맞아떨어진다면 정합성이 유지된다고 볼 수 있습니다. 그런 관점에서 보면, 지금까지는 정합성이 크게 깨졌다고 보기는 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 outbox 패턴은 굳이 적용할 필요가 없는 것일까요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;188&quot; data-end=&quot;355&quot;&gt;그렇지는 않습니다. 현재는 정합성이 잘 맞아 보일 수 있지만, 그렇다고 해서 장애 상황에 대비한 안전장치가 불필요하다는 뜻은 아닙니다.&lt;br /&gt;outbox 패턴은 평상시에 큰 차이를 드러내기보다는, 장애가 발생하더라도 이벤트 유실 없이 데이터와 메시지의 정합성을 최대한 유지하기 위한 장치에 가깝습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;357&quot; data-end=&quot;549&quot;&gt;이러한 관점에서 보면, outbox 패턴의 가치를 단순히 결과 수치의 차이로만 증명하기는 어려울 수 있습니다. 오히려 정합성을 위한 안전장치를 추가했음에도 시스템의 성능이 크게 저하되지 않았음을 보여주는 것 역시 충분히 의미 있는 접근입니다.&lt;br /&gt;즉, 안정성을 높이면서도 처리 성능을 일정 수준 이상 유지할 수 있음을 입증하는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;551&quot; data-end=&quot;740&quot;&gt;물론 시간이 충분하다면 kafka에 적재된 메시지는 결국 모두 소비될 가능성이 높습니다. 그러나 실제 운영 환경에서는 consumer 애플리케이션이 중단되거나, 메시지 처리 도중 장애가 발생하거나, 동일한 메시지가 중복 전달되는 상황도 고려해야 합니다. 이런 이유로 단순히 메시지를 발행하는 것만으로는 정합성을 충분히 보장하기 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;742&quot; data-end=&quot;856&quot;&gt;따라서 정합성을 더 높이기 위해서는 producer 측의 outbox 패턴뿐 아니라, consumer 측에서도 멱등성 보장, 재처리 전략, 실패 메시지 관리와 같은 추가적인 설계가 함께 이루어져야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;858&quot; data-end=&quot;1005&quot;&gt;결국 outbox 패턴이 없더라도 항상 정합성이 깨지는 것은 아닙니다. 다만 outbox 패턴은 데이터 저장과 이벤트 발행 사이에서 발생할 수 있는 불일치를 줄이고, 장애 상황에서도 정합성을 보다 명확하고 안전하게 보장하기 위한 방법이라고 볼 수 있습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-start=&quot;858&quot; data-end=&quot;1005&quot;&gt;&lt;b&gt;transactional outbox&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;858&quot; data-end=&quot;1005&quot;&gt;우리는 outbox 패턴을 이미 적용하고 있었습니다. 현재 구조는 transactional outbox 패턴을 기반으로, 비즈니스 데이터와 이벤트를 동일 트랜잭션으로 저장한 뒤, 커밋 이후 동일 요청 흐름에서 즉시 메시지를 발행하는 Inline 방식으로 구현되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bleCp6/dJMcabDyFLY/thZ83OSkWARzAMnGrgTEkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bleCp6/dJMcabDyFLY/thZ83OSkWARzAMnGrgTEkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bleCp6/dJMcabDyFLY/thZ83OSkWARzAMnGrgTEkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbleCp6%2FdJMcabDyFLY%2FthZ83OSkWARzAMnGrgTEkK%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;507&quot; height=&quot;373&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;940&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;하지만 현재는 메시지를 다시 발행해 주는 요소는 없습니다. 단순히 outbox와 발행을 하나의 트랜잭션 내에서 할 뿐입니다. 여기서 조금 더 정확히 말하면, 트랜잭션이 완료된 이후에 메시지를 발행해야 한다는 요구에 초점을 둔다면, afterCommit 방식도 하나의 선택지로 고려할 수 있습니다. 이제 저희가 고려해야 할 것은, 어떻게&amp;nbsp;하면&amp;nbsp;이러한&amp;nbsp;안전장치를&amp;nbsp;더&amp;nbsp;확실하게&amp;nbsp;만들&amp;nbsp;수&amp;nbsp;있을까&amp;nbsp;하는&amp;nbsp;점입니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;재시도 발행 메커니즘&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 방식은 이벤트를 outbox에 저장만 할 뿐, 그 값을 다시 발행하는 구조는 아닙니다. 따라서 consumer 측에 문제가 발생하더라도, outbox 데이터를 기반으로 복구를 진행할 수 없습니다. 결국 정합성을 더 확실하게 보장하려면, outbox에 저장된 이벤트를 다시 발행하는 메커니즘이 필요합니다. 이러한&amp;nbsp;방식으로는&amp;nbsp;재시도&amp;nbsp;발행&amp;nbsp;방식과&amp;nbsp;CDC&amp;nbsp;방식을&amp;nbsp;고려할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기에서는&amp;nbsp;우선&amp;nbsp;재시도&amp;nbsp;발행&amp;nbsp;방식을&amp;nbsp;중심으로&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;1964&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b60xIQ/dJMcafMKTaD/3FYlAnKpUvKk0Dob8rW9HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b60xIQ/dJMcafMKTaD/3FYlAnKpUvKk0Dob8rW9HK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b60xIQ/dJMcafMKTaD/3FYlAnKpUvKk0Dob8rW9HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb60xIQ%2FdJMcafMKTaD%2F3FYlAnKpUvKk0Dob8rW9HK%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;744&quot; height=&quot;409&quot; data-origin-width=&quot;1964&quot; data-origin-height=&quot;1080&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;1초에 한번씩 이벤트를 재 발행을 하고 있습니다. 일정량(BATCH_SIZE)을 정해서 FAIL 혹은 PANDING상태인 이벤트들만 가져옵니다. 그리고 재 발행을 시도하고 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일단 테스트를 돌려봅시다.&lt;br /&gt;성능 최적화 대상이 아닌 안전장치에 가깝긴하지만요. 성능 테스트는 의미는 상대적으로 크지 않다고 여겨집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biQySh/dJMcaiWWw2A/3st8jfuurKQXq9z2VjHWxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biQySh/dJMcaiWWw2A/3st8jfuurKQXq9z2VjHWxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biQySh/dJMcaiWWw2A/3st8jfuurKQXq9z2VjHWxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiQySh%2FdJMcaiWWw2A%2F3st8jfuurKQXq9z2VjHWxk%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;451&quot; height=&quot;503&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;1038&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;지금까지 DB에 데이터가 완전히 쌓인뒤에 테스트를 보내는 방식을 선택하였지만 이번에는 부하테스트는 DB의 데이터 여부와 상관없이 이벤트를 보내게 하였습니다. 그렇다는 것은 어느 정도 시간이 지났을 때 결과를 봤을 때 값이 일치하는지 여부를 확인해 봐야 하니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFv56h/dJMcaiQaLAd/f6Fdbda7g2b87sKgYOTza0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFv56h/dJMcaiQaLAd/f6Fdbda7g2b87sKgYOTza0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFv56h/dJMcaiQaLAd/f6Fdbda7g2b87sKgYOTza0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFv56h%2FdJMcaiQaLAd%2Ff6Fdbda7g2b87sKgYOTza0%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;1570&quot; height=&quot;124&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;124&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;총개수는 9737개로 위에서 발행된 이벤트의 개수와 일치한다는 것을 알 수 있습니다. 이로서 정합성은 맞추었습니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&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;b&gt;1. 1초마다 DB를 조회합니다.&lt;/b&gt;&lt;br /&gt;이는 불필요한 polling을 유발하고, DB 부하를 증가시켜 서비스 성능 저하의 원인이 될 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2. 처리 방식이 batch polling 중심입니다.&lt;/b&gt;&lt;br /&gt;현재&amp;nbsp;구조에서는&amp;nbsp;outbox&amp;nbsp;relay가&amp;nbsp;정해진&amp;nbsp;배치&amp;nbsp;단위로&amp;nbsp;이벤트를&amp;nbsp;조회&amp;middot;발행하므로,&amp;nbsp;&lt;br /&gt;상황에 따라 뒤에 있는 이벤트가 즉시 발행되지 못하고 다음 처리 주기까지 대기하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DuvXN/dJMcaaEFLnr/lq4XkY6RP1AiuVOhY1IW5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DuvXN/dJMcaaEFLnr/lq4XkY6RP1AiuVOhY1IW5K/img.png&quot; data-alt=&quot;이벤트 발행은 outbox뒤에서 처리가 되어집니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DuvXN/dJMcaaEFLnr/lq4XkY6RP1AiuVOhY1IW5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDuvXN%2FdJMcaaEFLnr%2Flq4XkY6RP1AiuVOhY1IW5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;609&quot; height=&quot;448&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이벤트 발행은 outbox뒤에서 처리가 되어집니다.&lt;/figcaption&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;b&gt;3. 이 구조만 놓고 보면 굳이 Kafka일 필요가 없습니다.&lt;/b&gt;&lt;br /&gt;현재 방식은 kafka의 고유한 특성을 적극적으로 활용한다기보다, DB polling 기반의 비동기 전달 구조에 kafka를 연결한 형태에 가깝습니다.&lt;br /&gt;&lt;br /&gt;따라서 동일한 방식은 다른 MQ에서도 구현 가능하며, 이 점에서 &quot;kafka를 kafka 답게 사용하고 있다&quot;라고 보기는 어렵습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;이쯤에서 테스트를 진행해봅시다.&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1046&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cz0PhJ/dJMcadVDsVp/OhPF3kIj8kQ2DQtY8KI7bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cz0PhJ/dJMcadVDsVp/OhPF3kIj8kQ2DQtY8KI7bk/img.png&quot; data-alt=&quot;이에 대한 내용은 재 테스트를 하는것이 좋다고 여겨집니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cz0PhJ/dJMcadVDsVp/OhPF3kIj8kQ2DQtY8KI7bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcz0PhJ%2FdJMcadVDsVp%2FOhPF3kIj8kQ2DQtY8KI7bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1046&quot; height=&quot;332&quot; data-origin-width=&quot;1046&quot; data-origin-height=&quot;332&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;1342&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qdpd8/dJMcadBiHXx/bdJXwuy5wR5CzjkrvW0kI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qdpd8/dJMcadBiHXx/bdJXwuy5wR5CzjkrvW0kI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qdpd8/dJMcadBiHXx/bdJXwuy5wR5CzjkrvW0kI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqdpd8%2FdJMcadBiHXx%2FbdJXwuy5wR5CzjkrvW0kI1%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;625&quot; height=&quot;202&quot; data-origin-width=&quot;1342&quot; data-origin-height=&quot;434&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;kafka가 꺼졌다가 켜졌음에도 정합성은 지켜졌다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;그렇다면 kafka답게 outbox 패턴을 사용하는 방법이 무엇일까요?&amp;nbsp;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가능하면 즉시 Kafka 발행하도록 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 publish가 되었을 때, 성공하였다면, outbox를 SENT로 실패하였다면, FAILED으로... 설정이 되어야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1982&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lNi2Y/dJMcaaYWpKP/KZEbVFy6u13wdw0HpTbhb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lNi2Y/dJMcaaYWpKP/KZEbVFy6u13wdw0HpTbhb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lNi2Y/dJMcaaYWpKP/KZEbVFy6u13wdw0HpTbhb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlNi2Y%2FdJMcaaYWpKP%2FKZEbVFy6u13wdw0HpTbhb0%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;704&quot; height=&quot;302&quot; data-origin-width=&quot;1982&quot; data-origin-height=&quot;850&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;현재는&amp;nbsp;트랜잭션&amp;nbsp;전파&amp;nbsp;방식을&amp;nbsp;사용해&amp;nbsp;별도의&amp;nbsp;트랜잭션을&amp;nbsp;생성하도록&amp;nbsp;구현했습니다. 다만 이 부분은 Spring Event 기반으로 분리해도 충분히 가능하다고 생각합니다. 그리고 해당 로직은 비동기로 처리해야 의미가 있는데, @Async가 적용되지 않으면 결국 동기적으로 실행됩니다. 이&amp;nbsp;경우&amp;nbsp;쿠폰&amp;nbsp;생성&amp;nbsp;이후&amp;nbsp;발행까지의&amp;nbsp;지연&amp;nbsp;시간이&amp;nbsp;예상보다&amp;nbsp;훨씬&amp;nbsp;커질&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;이렇게 되면, 평소 이벤트 흐름은 kafka가 담당하고 outbox는 실패 시 복구용으로 남겨 둘 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;outbox&amp;nbsp;poller는&amp;nbsp;느린&amp;nbsp;safety&amp;nbsp;net으로만&amp;nbsp;사용&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 outbox 복구 reply는 1s가 아닌 10s 이상으로 해도 괜찮습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BReWn/dJMcabjhAXJ/mKG1MjFuCMFBIKYBfQKlZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BReWn/dJMcabjhAXJ/mKG1MjFuCMFBIKYBfQKlZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BReWn/dJMcabjhAXJ/mKG1MjFuCMFBIKYBfQKlZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBReWn%2FdJMcabjhAXJ%2FmKG1MjFuCMFBIKYBfQKlZ1%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;1458&quot; height=&quot;232&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;232&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;이렇게까지 작성하면, 특정 MQ서비스에 구에 받지 않고 outbox패턴을 적용할 수 있습니다.&lt;br /&gt;하지만 저희는 kafka를 사용중에 있습니다. 그렇다면, 전통적인 outbox패턴 방식보다는 조금 더 kafka스럽게 작성하려면 어떤 방식으로 구현하는 것이 적절할까요?&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;재시도를 조금더 kafka 답게&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 재시도를 위해 outbox라는 안전장치를 두었습니다. 하지만 그렇다고 해서 이 구조가 kafka를 충분히 잘 활용하고 있다고 보기는 어렵습니다. 그렇다면 어떻게 해야 조금 더 kafka스럽게 만들 수 있을까요?&lt;br /&gt;우선 가장 기본적인 부분부터 살펴보기 위해, producer 옵션부터 조정해보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;producer 설정을 수정한다는 것은, 이벤트 발행 방식을 kafka의 특성에 더 맞게 다듬는다는 의미입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jg6U8/dJMcacCtVxN/sxcu3fGueKcwT6A3ghS6Pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jg6U8/dJMcacCtVxN/sxcu3fGueKcwT6A3ghS6Pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jg6U8/dJMcacCtVxN/sxcu3fGueKcwT6A3ghS6Pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJg6U8%2FdJMcacCtVxN%2Fsxcu3fGueKcwT6A3ghS6Pk%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;1444&quot; height=&quot;318&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;318&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;size14&quot;&gt;&lt;b&gt;acsks=0:&lt;/b&gt;&lt;br /&gt;- 가장 빠르게 전송할 수 있는 방식이지만, 유실 위험이 가장 큽니다.&amp;nbsp;&lt;br /&gt;- 지연&amp;nbsp;시간은&amp;nbsp;줄일&amp;nbsp;수&amp;nbsp;있지만,&amp;nbsp;메시지가&amp;nbsp;broker에&amp;nbsp;실제로&amp;nbsp;저장되었는지는&amp;nbsp;보장하지&amp;nbsp;않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;acsks=1:&lt;/b&gt;&lt;br /&gt;- 리더 broker가&amp;nbsp;메시지를&amp;nbsp;받으면&amp;nbsp;성공으로&amp;nbsp;판단합니다.&lt;br /&gt;- follower&amp;nbsp;replica에&amp;nbsp;반영될&amp;nbsp;때까지는&amp;nbsp;기다리지&amp;nbsp;않습니다.&lt;br /&gt;- 따라서&amp;nbsp;리더까지&amp;nbsp;전달되었다는&amp;nbsp;보장은&amp;nbsp;있지만,&amp;nbsp;리더 장애가&amp;nbsp;발생하면&amp;nbsp;유실&amp;nbsp;가능성이&amp;nbsp;있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;asks=all(-1):&lt;/b&gt;&lt;br /&gt;- 리더와 ISR(in-sync replicas)에 기록되어야 성공으로 판단합니다.&lt;br /&gt;- 가장&amp;nbsp;안전한&amp;nbsp;방식이지만,&amp;nbsp;상대적으로&amp;nbsp;지연&amp;nbsp;시간이&amp;nbsp;길어질&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저는 신뢰성이 중요하다고 생각하여 asks=all을 선택하였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;enable.idempotence=true&lt;/b&gt;&lt;br /&gt;- producer 재시도로 같은 레코드가 중복 저장되는 것을 막습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;retries=3&lt;/b&gt;&lt;br /&gt;- 전송 실패시 producer가 최대 3번 다시 시도 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;retry.backoff.ms=1000&lt;br /&gt;&lt;/b&gt;- 재시도 사이에 1초 기다립니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;이제는&amp;nbsp;consumer&amp;nbsp;측도&amp;nbsp;함께&amp;nbsp;살펴볼&amp;nbsp;필요가&amp;nbsp;있습니다.&lt;br /&gt;producer뿐 아니라 consumer까지 보완한다면, 수신 측 역시 kafka의 특성에 더 맞는 방향으로 구성할 수 있을 것 같습니다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;consumer에는 무엇을 넣을 수 있을까요?&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;consumer는 producer와 달리 이벤트를 보내는 역할이 아니라 받아서 처리하는 역할을 담당합니다. 따라서&amp;nbsp;이벤트를&amp;nbsp;어떤&amp;nbsp;방식으로&amp;nbsp;수신하고&amp;nbsp;처리할지에&amp;nbsp;대한&amp;nbsp;흐름을&amp;nbsp;이&amp;nbsp;부분에서&amp;nbsp;설계할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같은 것들을 추가할 수 있습니다.&lt;br /&gt;즉,consumer 측에서는 처리 실패에 대비한 retry/backoff, 최종 실패 메시지를 위한 DLT, 그리고 중복 소비를 방지하기 위한 idempotent consumer 전략을 함께 설계할 수 있습니다.&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;896&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mjsOr/dJMb99TgVef/ltWqTSvGbfd9SEyVPSED6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mjsOr/dJMb99TgVef/ltWqTSvGbfd9SEyVPSED6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mjsOr/dJMb99TgVef/ltWqTSvGbfd9SEyVPSED6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmjsOr%2FdJMb99TgVef%2FltWqTSvGbfd9SEyVPSED6k%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;514&quot; height=&quot;313&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;546&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;size14&quot;&gt;&lt;b&gt;@RetryableTopic&lt;/b&gt;: 실패한 메시지를 별도의 retry topic으로 보내고 시간이 지난 뒤 다시 소비하게 만드는 방식입니다.&lt;br /&gt;&lt;b&gt;DLT&lt;/b&gt;: 원본 메시지 처리 실패 -&amp;gt; retry 여러 번(default:3번) -&amp;gt; 실패 시 -&amp;gt; 더 이상 메인 흐름에서 처리하지 않고 DLT(Dead Letter Topic)으로 보냄&lt;br /&gt;&lt;b&gt;BackOff&lt;/b&gt;: 메시지 처리에 실패했다고 해서 바로 즉시 다시 처리하지 않고 조금&amp;nbsp;기다렸다가&amp;nbsp;재시도하게&amp;nbsp;만드는&amp;nbsp;방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;지수 백오프(Exponential backoff)&lt;/b&gt;: delay&amp;nbsp;값으로&amp;nbsp;시작하고,&amp;nbsp;재시도할&amp;nbsp;때마다&amp;nbsp;대기&amp;nbsp;시간이&amp;nbsp;multiplier만큼&amp;nbsp;증가하는&amp;nbsp;방식입니다.&lt;br /&gt;&lt;b&gt;고정 백오프(Fixed backoff)&lt;/b&gt;: 재시도할&amp;nbsp;때마다&amp;nbsp;항상&amp;nbsp;동일한&amp;nbsp;delay&amp;nbsp;간격으로&amp;nbsp;대기하는&amp;nbsp;방식입니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;DLT는 어떻게 처리가 되어지는가?&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btOzId/dJMcaivSRe0/tzaVcW2OOkIEMQSX4uQsbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btOzId/dJMcaivSRe0/tzaVcW2OOkIEMQSX4uQsbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btOzId/dJMcaivSRe0/tzaVcW2OOkIEMQSX4uQsbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtOzId%2FdJMcaivSRe0%2FtzaVcW2OOkIEMQSX4uQsbk%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;656&quot; height=&quot;138&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;194&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;최종적으로 더 이상 재시도를 수행하지 않는 경우에는, 이렇게 fallback 코드를 작성할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게까지 구성하면 어느 정도는 kafka답게 사용하고 있다고 말할 수 있는 수준에는 도달한거 같습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;약 2주간 쿠폰 발급 서비스를 개발하면서 많은 것을 학습을 했습니다. 그렇다고 해서 이게 native-kafka라고 말하기는 어렵다고 생각이 듭니다. 왜냐하면 운영쪽으로 많은 고민이 있어야 하지만 현재로써는 반영이 되지 않았습니다. 예를 들면, Consumer retry/DLT를 실제 운영 흐름, DLT 메시지 운영 체계등을 말할 수 있습니다. 게다가 CDC기반 outbox도 구현하지는 않았습니다. CDC기반으로 하게 되면 더 이상 풀링 방식으로 주입을 시켜줄 필요가 없습니다. 현재로써는 이 정도만 해도 큰 문제가 없다고 생각하였습니다. outbox가 없었을때도 정합성은 0%였기 때문입니다. 그래서 outbox때는 부하테스트한 내용이 없던거구요. 그렇다고 해서 여기서 마무리를 짓는다는 것은 아닙니다. 계속 언급했던 kafka답게를 실천하기 위해 앞으로도&amp;nbsp;꾸준히&amp;nbsp;개선해나갈&amp;nbsp;예정입니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/557</guid>
      <comments>https://b-programmer.tistory.com/557#entry557comment</comments>
      <pubDate>Wed, 25 Mar 2026 13:24:25 +0900</pubDate>
    </item>
    <item>
      <title>선착순 쿠폰 발급기 개발</title>
      <link>https://b-programmer.tistory.com/551</link>
      <description>&lt;figure id=&quot;og_1774412797752&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;당신의 동시성 테스트가 원하는 결과가 나오지 않는 이유&quot; data-og-description=&quot;TL;DR: 낙관락과 비관락을 고르는 기준에 대해 설명합니다.배경그 전에도 낙관락, 비관락을 해봤기때문에 금방할 줄 알았다.하지만 아니었다. 어디서 부터 문제 였을까?생각하기에는 비관락을 사&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/459&quot; data-og-url=&quot;https://b-programmer.tistory.com/459&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bj4BQ8/dJMb8YXK5Py/sQc0LKrTS04rAumWoyV6d1/img.jpg?width=288&amp;amp;height=175&amp;amp;face=0_0_288_175,https://scrap.kakaocdn.net/dn/bFv9ut/dJMb8VNuX2P/oRiKn0nzYlKWaKvgYnF400/img.jpg?width=288&amp;amp;height=175&amp;amp;face=0_0_288_175,https://scrap.kakaocdn.net/dn/fee6P/dJMb8Rj1vzJ/6raJGOHIKp6s3FLm4pTPa1/img.png?width=2428&amp;amp;height=354&amp;amp;face=0_0_2428_354&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/459&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/459&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bj4BQ8/dJMb8YXK5Py/sQc0LKrTS04rAumWoyV6d1/img.jpg?width=288&amp;amp;height=175&amp;amp;face=0_0_288_175,https://scrap.kakaocdn.net/dn/bFv9ut/dJMb8VNuX2P/oRiKn0nzYlKWaKvgYnF400/img.jpg?width=288&amp;amp;height=175&amp;amp;face=0_0_288_175,https://scrap.kakaocdn.net/dn/fee6P/dJMb8Rj1vzJ/6raJGOHIKp6s3FLm4pTPa1/img.png?width=2428&amp;amp;height=354&amp;amp;face=0_0_2428_354');&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;당신의 동시성 테스트가 원하는 결과가 나오지 않는 이유&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;TL;DR: 낙관락과 비관락을 고르는 기준에 대해 설명합니다.배경그 전에도 낙관락, 비관락을 해봤기때문에 금방할 줄 알았다.하지만 아니었다. 어디서 부터 문제 였을까?생각하기에는 비관락을 사&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때는 낙관락과 비관락에 대해서만 집중해서 테스트를 진행했던 거 같다.&lt;br /&gt;이번에는 차근차근 진행해 볼 예정이다. 낙관락도 비관락도 무엇인지 정확하게 알았으니 다른 테스트도 해도 크게 문제 될 건 없을 거 같다.&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일단 쿠폰을 한 장 준비해 둡니다. 이 쿠폰은 100장 제한이 걸려있으며, 불특정 한 사람들이 이 쿠폰 이용이 가능합니다.&lt;br /&gt;먼저 k6 스크립트를 준비해 주고 테스트를 진행해 봅니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;락을 걸기 전에&lt;/h2&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 선착순 쿠폰을 구하는 api는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1774412773493&quot; class=&quot;json&quot; style=&quot;background-color: #eeeeee; color: #222222; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;userId&quot;: 1
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;userId를 바꿔가면서 데이터를 넣어야 한다는 뜻입니다. 저는 겹치지 않게 userId를 100개 준비하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dA9oFw/dJMcafeXu1E/i50DDETEw4hFaxllfkBSwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dA9oFw/dJMcafeXu1E/i50DDETEw4hFaxllfkBSwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dA9oFw/dJMcafeXu1E/i50DDETEw4hFaxllfkBSwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdA9oFw%2FdJMcafeXu1E%2Fi50DDETEw4hFaxllfkBSwK%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;638&quot; height=&quot;190&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;초기 상태는 쿠폰이 100개가 남아있는 상태입니다. 이제 100명을 대상으로 테스트를 진행해 봅시다.&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;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVSNSH/dJMcabXTByn/jZKK2K0VOwjlSdsPHYfwmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVSNSH/dJMcabXTByn/jZKK2K0VOwjlSdsPHYfwmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVSNSH/dJMcabXTByn/jZKK2K0VOwjlSdsPHYfwmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVSNSH%2FdJMcabXTByn%2FjZKK2K0VOwjlSdsPHYfwmK%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;738&quot; height=&quot;103&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정상적으로 테스트가 완료가 되었습니다. 하지만 알 수 없는 오류가 발생하여 장상적인 결과가 나오지 않았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1774412773494&quot; class=&quot;routeros&quot; style=&quot;background-color: #eeeeee; color: #222222; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WARN[0000] [issue] failed status=500, body={&quot;success&quot;:false,&quot;data&quot;:null,&quot;message&quot;:&quot;일시적인 오류가 발생했습니다.&quot;}  source=console&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6UjqJ/dJMcafeXu1F/1CK3qFoGv6LJE1ayRvTrh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6UjqJ/dJMcafeXu1F/1CK3qFoGv6LJE1ayRvTrh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6UjqJ/dJMcafeXu1F/1CK3qFoGv6LJE1ayRvTrh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6UjqJ%2FdJMcafeXu1F%2F1CK3qFoGv6LJE1ayRvTrh1%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;745&quot; height=&quot;182&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;313&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;6장뿐만 성공했다고 나오고 나머지는 실패를 하였습니다. 생각보다 성공률이 높지는 않네요.&lt;br /&gt;이제 stock을 확인해 봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nQt7w/dJMcacicB8a/7eLXer5yVnCJe7ss2dIcS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nQt7w/dJMcacicB8a/7eLXer5yVnCJe7ss2dIcS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nQt7w/dJMcacicB8a/7eLXer5yVnCJe7ss2dIcS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnQt7w%2FdJMcacicB8a%2F7eLXer5yVnCJe7ss2dIcS1%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;261&quot; height=&quot;173&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이상합니다. 역시 6장이 성공했으면 94가 되어야 하는 게 아닐까요? 2장은 어디로 간 걸까요?&lt;br /&gt;그 이유는 쿠폰 2장에 대해 데드락이 발생이 하였다고 합니다. 이렇게 남은 수량은 신뢰도를 떨어뜨릴 수밖에 없습니다.&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 어떻게 해야 100장을 요청하면 100장이 모두 소진되게 할 수 있을까요?&lt;/p&gt;
&lt;h2 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;비관 락 사용&lt;/h2&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;초기상태로 되돌린 다음 테스트를 다시 진행하겠습니다. 똑같이 쿠폰아이디 1번으로 진행하였습니다.&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;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rvIyB/dJMcafeXu1H/BT1PNPAgoNpged2mxsKg9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rvIyB/dJMcafeXu1H/BT1PNPAgoNpged2mxsKg9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rvIyB/dJMcafeXu1H/BT1PNPAgoNpged2mxsKg9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrvIyB%2FdJMcafeXu1H%2FBT1PNPAgoNpged2mxsKg9K%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;551&quot; height=&quot;141&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 모든 쿠폰이 발급이 되었다는 것을 확인할 수 있었습니다.&lt;br /&gt;DB락에는 2가지 방법이 존재합니다.&lt;br /&gt;낙관락과 비관락입니다. 저는 비관락을 선택하여 사용했습니다. 그 이유는 모든 데이터를 전부 성공을 시켜야 하기 때문입니다.&lt;br /&gt;그렇다는 뜻은, 하나의 작업이 실행 중이라면, 나머지 작업들은 실행을 시키면 안 되기 때문입니다. 만약, 낙관락을 여기에서 사용한다면,&lt;br /&gt;하나만 성공하고 나머지는 실패하는 전략이기 때문에 여기에는 비관락을 선택을 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;472&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/smPUM/dJMcabXTByk/wKbamHfJERVFRziBj7jDuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/smPUM/dJMcabXTByk/wKbamHfJERVFRziBj7jDuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/smPUM/dJMcabXTByk/wKbamHfJERVFRziBj7jDuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsmPUM%2FdJMcabXTByk%2FwKbamHfJERVFRziBj7jDuK%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;324&quot; height=&quot;203&quot; data-origin-width=&quot;472&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿠폰이 100장을 100명에게 성공한 것을 확인하였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비관락은 제대로 걸린 거 같습니다. 이제 몇 가지 테스트를 더 진행해보려고 합니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;추가 테스트&lt;/h2&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 다음과 같은 상황에서도 똑같은 결과를 리턴할 수 있을까요?&amp;nbsp;&lt;br /&gt;1. 쿠폰이 100장이 아닌 10000장인 상황&lt;br /&gt;2. 선착순 쿠폰 10장을 5000명이 발급을 시도하는 상황&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;바로 진행해 봅시다.&lt;/p&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;쿠폰이 100장이 아닌 10000장인 상황&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;목적:&amp;nbsp;락&amp;nbsp;걸린&amp;nbsp;상태에서&amp;nbsp;처리량/지연(p95/p99)&amp;nbsp;확인&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;기대:&amp;nbsp;성공률&amp;nbsp;높음,&amp;nbsp;대신&amp;nbsp;동시성&amp;nbsp;높이면&amp;nbsp;지연&amp;nbsp;증가&amp;nbsp;가능&lt;/p&gt;
&lt;p style=&quot;color: #222222; 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;500&quot; data-origin-height=&quot;356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWPENm/dJMcabXTByi/V5K7wAe6YUtFnxXYFpCxVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWPENm/dJMcabXTByi/V5K7wAe6YUtFnxXYFpCxVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWPENm/dJMcabXTByi/V5K7wAe6YUtFnxXYFpCxVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWPENm%2FdJMcabXTByi%2FV5K7wAe6YUtFnxXYFpCxVK%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;435&quot; height=&quot;310&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;356&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; 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;2296&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAHpFk/dJMcabXTByo/XgSQdzXpZfUyzXtDROabnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAHpFk/dJMcabXTByo/XgSQdzXpZfUyzXtDROabnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAHpFk/dJMcabXTByo/XgSQdzXpZfUyzXtDROabnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAHpFk%2FdJMcabXTByo%2FXgSQdzXpZfUyzXtDROabnk%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;2296&quot; height=&quot;346&quot; data-origin-width=&quot;2296&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예상과 다르게 타임아웃이 발생하였다는 것을 확인할 수 있었습니다. 초반 테스트처럼 쿠폰 100장이 일부만 성공했던 그림과는 전혀 다른 그림이었습니다.&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;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WdCEY/dJMcacicB8c/wre0AYJiNKnFmIk3ncFv70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WdCEY/dJMcacicB8c/wre0AYJiNKnFmIk3ncFv70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WdCEY/dJMcacicB8c/wre0AYJiNKnFmIk3ncFv70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWdCEY%2FdJMcacicB8c%2Fwre0AYJiNKnFmIk3ncFv70%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;1824&quot; height=&quot;98&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만장모두 종료되었다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #222222; 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;762&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xrl92/dJMcacicB8e/X0UO3XEHjbWOGoSMwYNQ6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xrl92/dJMcacicB8e/X0UO3XEHjbWOGoSMwYNQ6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xrl92/dJMcacicB8e/X0UO3XEHjbWOGoSMwYNQ6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxrl92%2FdJMcacicB8e%2FX0UO3XEHjbWOGoSMwYNQ6k%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;370&quot; height=&quot;424&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;874&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;확인해 보니 &amp;nbsp; http_req_duration은 다음과 같다고 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1774412773495&quot; class=&quot;yaml&quot; style=&quot;background-color: #eeeeee; color: #222222; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- avg: 6.91초
- p95: 15.49초
- p99: null
- max: 16.29초&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;이&amp;nbsp;결과의&amp;nbsp;의미&lt;/h4&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 테스트에서는 총 10,000건의 쿠폰 발급 요청 중 7,629건이 성공하고 2,371건이 실패했습니다.&lt;br /&gt;그럼에도&amp;nbsp;불구하고&amp;nbsp;초과&amp;nbsp;발급은&amp;nbsp;발생하지&amp;nbsp;않았고,&amp;nbsp;초기&amp;nbsp;재고&amp;nbsp;10,000장에서&amp;nbsp;성공&amp;nbsp;건수&amp;nbsp;7,629건만큼만&amp;nbsp;차감되어&amp;nbsp;최종&amp;nbsp;재고가&amp;nbsp;2,371장으로&amp;nbsp;정확히&amp;nbsp;일치했습니다.&lt;br /&gt;&lt;br /&gt;즉, 성공 건수와 재고 감소량이 정확히 일치했고 oversold가 발생하지 않았으며 409(중복 발급), 400(잘못된 요청/발급 불가) 도 없었습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;이 점을 보면 쿠폰 발급 로직 자체는 동시성 상황에서도 재고 정합성을 안정적으로 유지했다고 볼 수 있습니다.&lt;br /&gt;&lt;/span&gt;반면, 실패한 2,371건은 모두 failureOther로 집계되었습니다.&lt;br /&gt;이는 비즈니스 규칙 위반이 아니라, 500 응답, 타임아웃, 커넥션 문제, 서버 처리 한계 초과 같은 시스템 과부하성 실패일 가능성이 높다는 뜻입니다.&lt;br /&gt;&lt;br /&gt;따라서 이 결과는 다음을 의미합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;쿠폰 발급 로직은 동시성 상황에서도 재고 정합성을 잘 지킨다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;하지만 10,000 VU 수준의 순간 스파이크 부하는 현재 시스템이 안정적으로 감당하지 못한다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;결과적으로 일부 요청은 성공했지만, 상당수는 애플리케이션/인프라 병목으로 인해 실패했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;한 줄 결론&lt;/h4&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정합성은 통과했지만, 10,000 VU 스파이크 상황에서 성능과 안정성은 실패했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;성능과 안전성을 올리면서 어떻게 해결할 수 있을까요?&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;선착순 쿠폰 10장을 5000명이 발급을 시도하는 상황&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;목적:&amp;nbsp;정합성(초과&amp;nbsp;발급&amp;nbsp;0)&amp;nbsp;검증&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;기대:&amp;nbsp;성공은&amp;nbsp;정확히&amp;nbsp;10건&amp;nbsp;근처,&amp;nbsp;나머지는&amp;nbsp;소진/중복&amp;nbsp;실패&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;핵심&amp;nbsp;체크:&amp;nbsp;issuedQuantity=10,&amp;nbsp;발급&amp;nbsp;row도&amp;nbsp;10,&amp;nbsp;초과발급&amp;nbsp;없음&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 상황은 정합성/경합&amp;nbsp;테스트를 뜻합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3vY1P/dJMcacicB79/icn5oXFbw8fSlO17OWdnfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3vY1P/dJMcacicB79/icn5oXFbw8fSlO17OWdnfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3vY1P/dJMcacicB79/icn5oXFbw8fSlO17OWdnfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3vY1P%2FdJMcacicB79%2Ficn5oXFbw8fSlO17OWdnfk%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;380&quot; height=&quot;277&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;10장이 만들어졌습니다.&lt;br /&gt;예상외의 결과가 나왔습니다. 의외로 나쁘지 않게 나와 당황스러운대요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTUzhV/dJMcacicB8b/lPNrTGCjk8NeCTp3itZA80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTUzhV/dJMcacicB8b/lPNrTGCjk8NeCTp3itZA80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTUzhV/dJMcacicB8b/lPNrTGCjk8NeCTp3itZA80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTUzhV%2FdJMcacicB8b%2FlPNrTGCjk8NeCTp3itZA80%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;385&quot; height=&quot;438&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만 조금 이상한 부분이 존재합니다. 바로 failOther부분인데요.&lt;br /&gt;이 부분은 409 말고 다른 실패라서, 약간의 시스템성 실패(500/timeout 등)가 섞여 있을 가능성이 있습니다.&lt;br /&gt;&lt;br /&gt;즉 failureOther가 잡혔다는 건, 비즈니스적으로 의도된 탈락(재고 소진/중복 등)이 아니라 정상적이지 않은 추가 실패가 섞였다는 뜻으로 보는 게 맞습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;한 줄 결론&lt;/h4&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 결과가 의미하는 것은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #222222; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;쿠폰 발급 로직은 극심한 경쟁 상황에서도 초과 발급 없이 정확히 동작했다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;대부분의 실패는 의도된 경쟁 탈락(409)으로 처리되었다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;하지만 일부 요청은 시스템 과부하성 실패(failureOther)로 떨어져 완전한 안정성 확보에는 미치지 못했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 두 가지 테스트를 통해 확인할 수 있었던 고무적인 점은, 쿠폰 재고가 음수로 내려가는 초과 발급 현상이 발생하지 않았다는 것입니다. 이는 동시성 제어가 의도대로 동작했다는 의미이며, 비관적 락 적용이 정합성 보장에 유효했을 가능성을 보여줍니다. 반면, 초과 발급은 방지했지만 여전히 일부 요청에서 실패가 발생했고 응답 시간도 크게 증가한 것으로 보아, 정합성 문제는 해결되었더라도 성능 및 안정성 이슈는 여전히 남아있다고 판단할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떻게 하면 해결할 수 있을까요?&lt;/p&gt;</description>
      <category>개발/동시성 테스트</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/551</guid>
      <comments>https://b-programmer.tistory.com/551#entry551comment</comments>
      <pubDate>Mon, 23 Mar 2026 21:22:49 +0900</pubDate>
    </item>
    <item>
      <title>결제 성공 콜백의 동시성 처리 안정화</title>
      <link>https://b-programmer.tistory.com/555</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;분산락을 적용한 뒤 부하 테스트를 진행하는 과정에서 5XX 응답이 과도하게 발생하는 현상을 확인했습니다. 이번 작업의 목적은 동일한 결제 성공 콜백이 동시에 들어오는 상황에서 분산락이 의도한 대로 동작하는지 검증하고, 그 결과를 해석 가능하게 만드는 것이었습니다. 문제는 5XX가 발생했을 때 그것이 분산락 경합에 의한 결과인지, 아니면 락 외부의 예외인지 구분하기 어려웠다는 점이었습니다. 따라서 이번 작업에서는 결제 기능 전체의 안정성 개선보다, 분산락 동작을 방해하는 노이즈를 줄이고, 락 경쟁 결과를 명확히 관측할 수 있도록 만드는 것에 초점을 맞췄습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1930&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDDyEm/dJMb99Z2ZUK/SKT405Z5vlBAJinx2gZmiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDDyEm/dJMb99Z2ZUK/SKT405Z5vlBAJinx2gZmiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDDyEm/dJMb99Z2ZUK/SKT405Z5vlBAJinx2gZmiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDDyEm%2FdJMb99Z2ZUK%2FSKT405Z5vlBAJinx2gZmiK%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;1930&quot; height=&quot;874&quot; data-origin-width=&quot;1930&quot; data-origin-height=&quot;874&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;2338&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8XBfj/dJMcabwJHGv/4BDbXmiIBSL0rOVWSekVZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8XBfj/dJMcabwJHGv/4BDbXmiIBSL0rOVWSekVZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8XBfj/dJMcabwJHGv/4BDbXmiIBSL0rOVWSekVZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8XBfj%2FdJMcabwJHGv%2F4BDbXmiIBSL0rOVWSekVZk%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;2338&quot; height=&quot;602&quot; data-origin-width=&quot;2338&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 200UV를 기준으로 잡고 k6를 돌려보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;30분간 200UV를 돌린결과 5XX에러가 19950번이 발생이 되었다는 것을 알 수 있습니다.&lt;br /&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;2654&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIwjTS/dJMcagdLWyr/vsmNISMESgs8cm4kKzB0b1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIwjTS/dJMcagdLWyr/vsmNISMESgs8cm4kKzB0b1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIwjTS/dJMcagdLWyr/vsmNISMESgs8cm4kKzB0b1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIwjTS%2FdJMcagdLWyr%2FvsmNISMESgs8cm4kKzB0b1%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;2654&quot; height=&quot;364&quot; data-origin-width=&quot;2654&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 진행해보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사전 작업&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 테스트하려고 하는 부분은 결제이후에 토스 api를 통해 구독을 처리하는 부분입니다.&lt;br /&gt;테스트 하기전에 두 가지 문제가 발생하였습니다. 테스트를 하기 위해서는 로그인이 필수불가하게 필요하다.&lt;br /&gt;두 번째, 토스 API로 직접 부하테스트를 진행할 수 없다.&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;1158&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xb4SJ/dJMcacCrAGB/wS88yjyFJvxaKHHdSb1GbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xb4SJ/dJMcacCrAGB/wS88yjyFJvxaKHHdSb1GbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xb4SJ/dJMcacCrAGB/wS88yjyFJvxaKHHdSb1GbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxb4SJ%2FdJMcacCrAGB%2FwS88yjyFJvxaKHHdSb1GbK%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;714&quot; height=&quot;260&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강제로 특정 프로파일인 경우 헤더에 EMAIL표기가 되어있다면, 로그인 절차를 거치지 않고 세션에 헤더에서 읽어온 이메일을 반영해주는걸로 변경하였습니다. 그래서 특정 이메일로 회원가입을 하고, 그것으로 로그인을 하는 절차를 지웠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 번째&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접적으로 토스API를 테스트하는 것이 아닌 Fake혹은 Mock객체를 만들어서 진행하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AP3dt/dJMcac3ttDH/LthaxU0zeSu3SHsuQYYRMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AP3dt/dJMcac3ttDH/LthaxU0zeSu3SHsuQYYRMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AP3dt/dJMcac3ttDH/LthaxU0zeSu3SHsuQYYRMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAP3dt%2FdJMcac3ttDH%2FLthaxU0zeSu3SHsuQYYRMk%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;767&quot; height=&quot;215&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 실제 토스 API를 사용하는 곳에서는 @Profile(&quot;!dev&quot;)로 설정해놓았습니다.&lt;br /&gt;어째서 프로파일이 test나 local이 아닌 이유는 저는 스프링 부트를 직접적으로 돌리는 것이 아닌 도커로 띄워서 돌리는 것을 선택하였습니다. 도커로 띄우는 경우에는 프로파일을 dev로 설정하여서 분산락 테스트는 dev로 하기로 결정하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 도커 앱을 3개를 띄우고 시작하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tnLEr/dJMcaiWUxhD/dSXsOjwOkeuDFtf9inhfm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tnLEr/dJMcaiWUxhD/dSXsOjwOkeuDFtf9inhfm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tnLEr/dJMcaiWUxhD/dSXsOjwOkeuDFtf9inhfm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtnLEr%2FdJMcaiWUxhD%2FdSXsOjwOkeuDFtf9inhfm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;831&quot; height=&quot;141&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 되면 시작 포인트가 8083,8084,8085 이렇게 3개로 고정이 되어집니다. 사실 우리가 웹을 들어가게 되면&lt;br /&gt;8083으로도 들어갈수도 있고, 8084로도 들어갈수도 있고, 8085로 들어가게 설정하지는 않습니다.&lt;br /&gt;단 하나의 시작 포인트로 들어가게 됩니다. 그렇다고 해서 3개중 하나가 리더가 되는 형태는 전혀아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 시작 포인트에서 들어가기 위해서는 로드벨런서라는것이 필요합니다.&lt;br /&gt;이는 부하를 고르게 전달해주는 장치라고 생각하시면 됩니다. 저는 nginx로 롤링배포를 사용해서 8083,8084,8085를 사용하게 해두었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eOeQox/dJMcahKxcJX/aVkPr6FP1jcdbm2vC9AI91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eOeQox/dJMcahKxcJX/aVkPr6FP1jcdbm2vC9AI91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eOeQox/dJMcahKxcJX/aVkPr6FP1jcdbm2vC9AI91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeOeQox%2FdJMcahKxcJX%2FaVkPr6FP1jcdbm2vC9AI91%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;681&quot; height=&quot;285&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;542&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;1168&quot; data-origin-height=&quot;586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uaFyT/dJMcahqdvd7/83k73z2jNXx241rRGKo0tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uaFyT/dJMcahqdvd7/83k73z2jNXx241rRGKo0tK/img.png&quot; data-alt=&quot;dragons1:8083 dragon2:8084 dragon3: 8085&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uaFyT/dJMcahqdvd7/83k73z2jNXx241rRGKo0tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuaFyT%2FdJMcahqdvd7%2F83k73z2jNXx241rRGKo0tK%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;563&quot; height=&quot;282&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;586&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;dragons1:8083 dragon2:8084 dragon3: 8085&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 30분간 200UVS로 잡고 테스트를 진행하였습니다.&lt;br /&gt;부하 테스트를 진행하던 과정에서 5XX 응답이 과도하게 발생하는 문제를 확인했습니다. 물론 부하 상황에서 일부 5XX가 발생하는 것은 가능하지만, 문제는 해당 오류가 분산락충돌 이후 중복 처리 과정에서 발생한 것인지, 혹은 락과 무관한 다른 원인 때문인지 즉시 구분하기 어려웠다는 점이었습니다. 그래서 동일한 orderId에 대한 동시 confirm 요청이&amp;nbsp;들어오더라도,&amp;nbsp;분산락&amp;nbsp;경합&amp;nbsp;결과가&amp;nbsp;예측&amp;nbsp;가능한&amp;nbsp;응답으로&amp;nbsp;처리되도록&amp;nbsp;로직을&amp;nbsp;보완했습니다.&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;1.&lt;span&gt; 동일한 orderId에 대한 동시 confirm 요청이 들어오더라도, 분산락 경합 결과가 409 Conflict 등 예측 가능한 응답으로 처리되도록 한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;2.&lt;span&gt; 5XX&amp;nbsp;응답&amp;nbsp;발생&amp;nbsp;시,&amp;nbsp;분산락&amp;nbsp;경합에&amp;nbsp;의한&amp;nbsp;문제인지&amp;nbsp;락&amp;nbsp;외부&amp;nbsp;문제인지&amp;nbsp;구분&amp;nbsp;가능하도록&amp;nbsp;한다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;3. 30분 baseline 테스트에서 분산락 관련 메트릭과 응답 패턴을 안정적으로 검증할 수 있도록 한다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;동일한&amp;nbsp;orderId에&amp;nbsp;대한&amp;nbsp;동시&amp;nbsp;confirm&amp;nbsp;요청이&amp;nbsp;들어오더라도,&amp;nbsp;분산락&amp;nbsp;경합&amp;nbsp;결과가&amp;nbsp;409&amp;nbsp;Conflict&amp;nbsp;등&amp;nbsp;예측&amp;nbsp;가능한&amp;nbsp;응답으로&amp;nbsp;처리되도록&amp;nbsp;한다.&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스 결제시스템은 다음과 같은 절차를 가집니다. &amp;nbsp;&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;1278&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chL1ZP/dJMcacvG1xL/9cOVgXcGFk2MAEgPcggavk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chL1ZP/dJMcacvG1xL/9cOVgXcGFk2MAEgPcggavk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chL1ZP/dJMcacvG1xL/9cOVgXcGFk2MAEgPcggavk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchL1ZP%2FdJMcacvG1xL%2F9cOVgXcGFk2MAEgPcggavk%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;794&quot; height=&quot;132&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스 UI에서 success API로 리다이렉트될 때 paymentKey가 전달됩니다. 이 값을 이용해 토스 승인 API를 호출하고, 결제가 정상적으로 완료됐는지 확인할 수 있습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;paymentKey는 토스 결제를 식별하는 고유값이므로 중복되면 안 됩니다.하지만 현재는 이에 대한 제약이 없습니다.&amp;nbsp;우리 시스템은 토스 UI에서 바로 결제 요청을 보내는 구조가 아니라, 먼저 결제 API를 호출해 결제 데이터를 READY(대기) 상태로 저장합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 결제 전에는 paymentKey가 null일 수밖에 없습니다. 이후 토스 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;성공 리다이렉트 시점&lt;/span&gt;에 전달받은 paymentKey를 저장하게 되는데, 이때 동일한 paymentKey가 중복 저장되면 동일 결제가 여러 번 처리될 위험이 있습니다. 그래서&amp;nbsp;paymentKey에는&amp;nbsp;유니크&amp;nbsp;제약이&amp;nbsp;반드시&amp;nbsp;필요합니다.&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;1288&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ph5l8/dJMcahqdA2f/nAzTGcwgjJ5y2LMkXkBI90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ph5l8/dJMcahqdA2f/nAzTGcwgjJ5y2LMkXkBI90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ph5l8/dJMcahqdA2f/nAzTGcwgjJ5y2LMkXkBI90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fph5l8%2FdJMcahqdA2f%2FnAzTGcwgjJ5y2LMkXkBI90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;621&quot; height=&quot;187&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 기존 payment가 등록이 되어있다면, 409예외를 터드리도록 하였습니다.&lt;br /&gt;하지만 이럼에도 불구하고 DB측에서 먼저 데이터가 저장이 될 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kqdtd/dJMcaiCC8SH/VwbasqKxHQMw6N38yjMG7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kqdtd/dJMcaiCC8SH/VwbasqKxHQMw6N38yjMG7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kqdtd/dJMcaiCC8SH/VwbasqKxHQMw6N38yjMG7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKqdtd%2FdJMcaiCC8SH%2FVwbasqKxHQMw6N38yjMG7k%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;781&quot; height=&quot;99&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;188&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB측에도 paymentKey를 unique로 등록시켜두었습니다. 하지만 이렇게 설정하다보면 동일한 payment가 저장이 되어진다면, 409가 아님 5XX에러가 발생이 되어집니다. 이를 방지하고자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WZ1br/dJMcaiP83WW/jk6ummDPw9N2EFgJlJfBZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WZ1br/dJMcaiP83WW/jk6ummDPw9N2EFgJlJfBZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WZ1br/dJMcaiP83WW/jk6ummDPw9N2EFgJlJfBZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWZ1br%2FdJMcaiP83WW%2Fjk6ummDPw9N2EFgJlJfBZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;777&quot; height=&quot;205&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 완료 처리 시 paymentKey를 저장합니다. paymentKey는 토스 결제를 식별하는 고유값이므로 중복되면 안 됩니다. 따라서 저장 과정에서 중복으로 인해 DataIntegrityViolationException이 발생하면,&amp;nbsp;서버 내부 오류(5XX)로 두지 않고 409 Conflict로 변환해&amp;nbsp;이미 처리된 결제 요청임을 명확하게 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그러면 어째서 saveAndFlush를 했냐라고 한다면?&lt;/b&gt;&lt;br /&gt;paymentKey 중복 여부는 애플리케이션 메모리에서 확인되는 것이 아니라 DB의 유니크 제약으로 최종 보장됩니다. 따라서 단순히 엔티티 값만 변경하는 것으로는 부족하고, saveAndFlush()를&amp;nbsp;통해&amp;nbsp;DB&amp;nbsp;반영&amp;nbsp;시점을&amp;nbsp;앞당겨야&amp;nbsp;중복&amp;nbsp;충돌을&amp;nbsp;현재&amp;nbsp;로직&amp;nbsp;안에서&amp;nbsp;감지할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;-&amp;gt; saveAndFlush()를 쓴 이유는 중복 예외를 트랜잭션 커밋 시점이 아니라 현재 confirm 처리 흐름 안에서 잡기 위해서입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업을 한다고 해서 5XX예외가 4XX예외로 바뀌지는 않습니다. 하지만 조금더 &lt;u&gt;동일한 orderId에 대한 동시 confirm 요청이 들어와도, 분산락 경합 결과를 예측 가능한 응답으로 처리하도록 개선&lt;/u&gt;하는것이 목적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 10분정도만 테스트를 진행해보겠습니다. 이걸 한다고 해서 전에꺼랑 비교는 어렵지만 말이지만 말입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2660&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R7c3c/dJMcaakjWDg/8XXIbgdklNxtR4nOARN9qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R7c3c/dJMcaakjWDg/8XXIbgdklNxtR4nOARN9qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R7c3c/dJMcaakjWDg/8XXIbgdklNxtR4nOARN9qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR7c3c%2FdJMcaakjWDg%2F8XXIbgdklNxtR4nOARN9qk%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;2660&quot; height=&quot;246&quot; data-origin-width=&quot;2660&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 예상한거와 달리 아직도 예상치 못한 오류가 발생이 되어지는군요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1976&quot; data-origin-height=&quot;1266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QRM0b/dJMcahcFr7V/gBKD98TSn1FivM7VcDGUSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QRM0b/dJMcahcFr7V/gBKD98TSn1FivM7VcDGUSK/img.png&quot; data-alt=&quot;10m 테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QRM0b/dJMcahcFr7V/gBKD98TSn1FivM7VcDGUSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQRM0b%2FdJMcahcFr7V%2FgBKD98TSn1FivM7VcDGUSK%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;674&quot; height=&quot;432&quot; data-origin-width=&quot;1976&quot; data-origin-height=&quot;1266&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;10m 테스트&lt;/figcaption&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;5XX&amp;nbsp;응답&amp;nbsp;발생&amp;nbsp;시,&amp;nbsp;분산락&amp;nbsp;경합에&amp;nbsp;의한&amp;nbsp;문제인지&amp;nbsp;락&amp;nbsp;외부&amp;nbsp;문제인지&amp;nbsp;구분&amp;nbsp;가능하도록&amp;nbsp;한다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 분산락 처리 과정에서 발생한 오류와 락 외부에서 발생한 오류가 모두 5XX로 동일하게 응답되어, 원인 구분이 어려웠습니다. 그 결과, 장애가 분산락 경합/처리 문제인지 아니면 다른 내부 오류인지 파악하기 어려운 경우가 많았습니다. 이번에는&amp;nbsp;이&amp;nbsp;둘을&amp;nbsp;구분&amp;nbsp;가능한&amp;nbsp;응답으로&amp;nbsp;분리해,&amp;nbsp;문제&amp;nbsp;원인을&amp;nbsp;더&amp;nbsp;명확하게&amp;nbsp;식별할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;개선하려고&amp;nbsp;합니다.&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;1574&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rj6sf/dJMcacPYsct/vIkw9cuDBWJia9FApr1vcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rj6sf/dJMcacPYsct/vIkw9cuDBWJia9FApr1vcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rj6sf/dJMcacPYsct/vIkw9cuDBWJia9FApr1vcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frj6sf%2FdJMcacPYsct%2FvIkw9cuDBWJia9FApr1vcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;839&quot; height=&quot;161&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 분산락을 얻는 도중 문제가 발생이 되어진다면, LockException이 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 분산락에서 발생한 예외랑 그렇지 않는 예외를 구분을 지을 수 있게 되었습니다. 이번에도 10분간 테스트를 진행해보았습니다.&lt;br /&gt;놀랍게도 일시적인 오류는 발생하지 않았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;906&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYQbGn/dJMcadadwDF/E3Rj5VrYaODZZHm7TKEZEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYQbGn/dJMcadadwDF/E3Rj5VrYaODZZHm7TKEZEK/img.png&quot; data-alt=&quot;10m&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYQbGn/dJMcadadwDF/E3Rj5VrYaODZZHm7TKEZEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYQbGn%2FdJMcadadwDF%2FE3Rj5VrYaODZZHm7TKEZEK%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;751&quot; height=&quot;359&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;906&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;10m&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;30분 baseline 테스트에서 분산락 관련 메트릭과 응답 패턴을 안정적으로 검증할 수 있도록 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다시 30분동안 재 테스트하면서 얼마나 변화하였는지 살펴봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2948&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mvKWA/dJMcagY7Vvn/KJ66O9X4gK5lJobCEL1Onk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mvKWA/dJMcagY7Vvn/KJ66O9X4gK5lJobCEL1Onk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mvKWA/dJMcagY7Vvn/KJ66O9X4gK5lJobCEL1Onk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmvKWA%2FdJMcagY7Vvn%2FKJ66O9X4gK5lJobCEL1Onk%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;2948&quot; height=&quot;576&quot; data-origin-width=&quot;2948&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적인 5XX에러가 노출되지 않았습니다. 그렇다고 해서 100%없어진것은 아니네요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;1028&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RwZkn/dJMcai3Fre9/pMOVIeR8wFduhsbGGZkeq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RwZkn/dJMcai3Fre9/pMOVIeR8wFduhsbGGZkeq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RwZkn/dJMcai3Fre9/pMOVIeR8wFduhsbGGZkeq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRwZkn%2FdJMcai3Fre9%2FpMOVIeR8wFduhsbGGZkeq0%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;1942&quot; height=&quot;1028&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;1028&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상치못하게 예외가 발생하는 경우를 8.23%에서 0.02%로 감소 시켰습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CwZWL/dJMb99Z21gc/XyLJ5MM4zJ7ZTDz0WGg8G0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CwZWL/dJMb99Z21gc/XyLJ5MM4zJ7ZTDz0WGg8G0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CwZWL/dJMb99Z21gc/XyLJ5MM4zJ7ZTDz0WGg8G0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCwZWL%2FdJMb99Z21gc%2FXyLJ5MM4zJ7ZTDz0WGg8G0%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;818&quot; height=&quot;157&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;310&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;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스&amp;nbsp;트랜잭션은&amp;nbsp;여러&amp;nbsp;Redis&amp;nbsp;명령을&amp;nbsp;하나의&amp;nbsp;작업&amp;nbsp;단위처럼&amp;nbsp;묶어&amp;nbsp;실행하는&amp;nbsp;기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 MULTI / EXEC 를 사용합니다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;MULTI&amp;nbsp;:&amp;nbsp;트랜잭션&amp;nbsp;시작&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;이후&amp;nbsp;명령들:&amp;nbsp;바로&amp;nbsp;실행되지&amp;nbsp;않고&amp;nbsp;큐에&amp;nbsp;쌓임&lt;br /&gt;&amp;nbsp;&amp;nbsp;-&amp;nbsp;EXEC&amp;nbsp;:&amp;nbsp;쌓인&amp;nbsp;명령을&amp;nbsp;순서대로&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;1390&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmp58m/dJMcah4Pp5Y/kY8K9vLU2iGo7Pc1zNhWwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmp58m/dJMcah4Pp5Y/kY8K9vLU2iGo7Pc1zNhWwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmp58m/dJMcah4Pp5Y/kY8K9vLU2iGo7Pc1zNhWwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcmp58m%2FdJMcah4Pp5Y%2FkY8K9vLU2iGo7Pc1zNhWwK%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;550&quot; height=&quot;430&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;1086&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;1924&quot; data-origin-height=&quot;1060&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjPx7f/dJMcaiilBtT/GiFSC6yS8C3b9KJz1SyJo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjPx7f/dJMcaiilBtT/GiFSC6yS8C3b9KJz1SyJo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjPx7f/dJMcaiilBtT/GiFSC6yS8C3b9KJz1SyJo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjPx7f%2FdJMcaiilBtT%2FGiFSC6yS8C3b9KJz1SyJo1%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;699&quot; height=&quot;385&quot; data-origin-width=&quot;1924&quot; data-origin-height=&quot;1060&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각한거보다 성능이 너무 처참하네요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r7ftK/dJMcadH4yTV/UEzVk57SDZ20pku8ZrpkI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r7ftK/dJMcadH4yTV/UEzVk57SDZ20pku8ZrpkI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r7ftK/dJMcadH4yTV/UEzVk57SDZ20pku8ZrpkI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr7ftK%2FdJMcadH4yTV%2FUEzVk57SDZ20pku8ZrpkI1%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;820&quot; height=&quot;346&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 시간은 나쁘지는 않은거 같은데 22:30분 전후로 p99가 좀 많이 우뚝 서있는거 말고는 괜찮아보입니다.&lt;br /&gt;하지만&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FIKIH/dJMcach86Cb/8hRIYTsJUwe2kIUHgvrdS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FIKIH/dJMcach86Cb/8hRIYTsJUwe2kIUHgvrdS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FIKIH/dJMcach86Cb/8hRIYTsJUwe2kIUHgvrdS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFIKIH%2FdJMcach86Cb%2F8hRIYTsJUwe2kIUHgvrdS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;710&quot; height=&quot;283&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;594&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;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면, 레디슨은 어떨까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디슨(Redisson)은&amp;nbsp;Java에서&amp;nbsp;Redis를&amp;nbsp;더&amp;nbsp;쉽게&amp;nbsp;쓰게&amp;nbsp;해주는&amp;nbsp;고수준&amp;nbsp;클라이언트&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;1428&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buAFjG/dJMcaflDvO5/sTM0gHT8WPJoqeBUug6Zmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buAFjG/dJMcaflDvO5/sTM0gHT8WPJoqeBUug6Zmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buAFjG/dJMcaflDvO5/sTM0gHT8WPJoqeBUug6Zmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuAFjG%2FdJMcaflDvO5%2FsTM0gHT8WPJoqeBUug6Zmk%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;709&quot; height=&quot;311&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 실패율은 발생하지 않았네요..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1974&quot; data-origin-height=&quot;1004&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMYoSa/dJMcabDxH63/D2U9UUYVo6NaKBUlQo29m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMYoSa/dJMcabDxH63/D2U9UUYVo6NaKBUlQo29m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMYoSa/dJMcabDxH63/D2U9UUYVo6NaKBUlQo29m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMYoSa%2FdJMcabDxH63%2FD2U9UUYVo6NaKBUlQo29m1%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;766&quot; height=&quot;390&quot; data-origin-width=&quot;1974&quot; data-origin-height=&quot;1004&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나쁘지는 않지만 예상치못한 예외가 발생하는 경우가 0.04%로 루아만 사용했을때보다 높게나왔네요.&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;원래는 분산락 자체를 더 근본적으로 조정하는 방향을 생각했지만, 이번에는 Lua 스크립트 기반 락 처리만으로도 충분히 의미 있는 개선을 확인할 수 있었습니다. &amp;nbsp;초반에는 예상치 못한 오류가 발생했는데, 원인을 확인해보니 DB의 unique 제약 예외가 그대로 500 응답으로 노출되고 있었습니다. 즉, 실제 문제는 분산락 자체라기보다 동시 처리 과정에서 발생한 DB 중복 예외가 적절히 변환되지 않았던 점에 있었습니다. 이후 DB 중복 예외 발생 시 이를 409 Conflict로 처리하도록 보완한 결과, 비정상 응답 비율을 8.23%에서 0.02%로 감소시킬 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/555</guid>
      <comments>https://b-programmer.tistory.com/555#entry555comment</comments>
      <pubDate>Fri, 20 Mar 2026 21:16:53 +0900</pubDate>
    </item>
    <item>
      <title>Stateless하게 개발하기</title>
      <link>https://b-programmer.tistory.com/554</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Stateless하게 개발한다는 것은 무엇을 의미할까? HTTP는 무상태(stateless)라고 하고, 아키텍처에서도 stateless라는 말이 자주 등장한다. 처음에는 이 개념이 잘 이해되지 않았다. 왜냐하면 상태를 가진다는 것은 결국 어딘가에 데이터가 저장된다는 의미이기 때문이다. 그렇다면 DB나 Redis 같은 저장소를 사용하는 순간 stateless하지 않은 것이 아닌가라는 생각이 들었다. 하지만 이는 잘못된 이해였다. Stateless의 기준은 인프라 전체가 아니라 서버이다. DB나 Redis를 사용하는 것은 상태를 없애는 것이 아니라, 서버 외부로 분리하는 것이다. 즉, stateless하게 개발한다는 것은 서버가 데이터를 전혀 저장하지 않는다는 의미가 아니라, 서버가 이전 요청의 상태를 기억하지 않도록 만드는 것&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이다. &lt;/span&gt;각 요청은 독립적으로 처리되어야 하며, 서버는 이전 요청이 무엇이었는지 알 필요가 없다. 필요한 상태는 요청에 포함되거나, DB나 Redis와 같은 외부 저장소에서 조회한다. 결국 stateless 아키텍처란 상태를 제거하는 것이 아니라, 상태를 서버 밖으로 이동시키는 설계 방식이라고 볼 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Stateless하게 개발한다는것은 무슨말일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stateless하게 개발한다는 것은 무엇을 의미할까?&lt;br /&gt;Stateless하게 개발한다는 것은 서버가 이전 요청을 기억하지 않도록 만드는 것이다.&lt;br /&gt;간단하게 말하면 stateless는 '상태 없음'으로 보이지만, 실제로는 서버가 상태를 가지고 있지 않다는 의미에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 stateful하다는 것은 무엇일까? 상태를 저장한다는 의미를 가지고 있는데, 다음의 예시를 보자.&lt;br /&gt;사용자의 로그인 여부를 확인해야 하는 상황을 생각해보면, stateful 구조에서는 서버가 사용자 세션을 메모리에 저장한다.&lt;br /&gt;이 경우 해당 사용자의 이후 요청은 같은 서버로 전달되어야 한다.&lt;br /&gt;왜냐하면 상태가 특정 서버에 있기 때문이다. 따라서 서버가 죽으면 세션 정보도 함께 사라지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 stateless 구조에서는 서버가 &quot;나는 아무것도 기억하지 않는다. 필요한 정보를 요청에 담아달라&quot;고 가정한다.&lt;br /&gt;요청마다 토큰이나 사용자 ID가 포함되고, 필요한 정보가 있다면 DB나 Redis 같은 외부 저장소에서 조회한다. 즉, 특정 서버에 상태가 묶이지 않기 때문에 어떤 서버든 동일하게 요청을 처리할 수 있다. 결국 stateless 아키텍처란 상태를 없애는 것이 아니라, 상태를 서버 외부로 분리하는 방식이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 확실하게 보여줄수있는 예시가 바로 세션 로그인과 JWT로그인이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5WGMq/dJMcagkvqu4/XIcgNnBJ845037DPL57bD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5WGMq/dJMcagkvqu4/XIcgNnBJ845037DPL57bD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5WGMq/dJMcagkvqu4/XIcgNnBJ845037DPL57bD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5WGMq%2FdJMcagkvqu4%2FXIcgNnBJ845037DPL57bD1%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;623&quot; height=&quot;415&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 해서 Staless가 마냥좋고 Stateful이 마냥 나쁜것은 아니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Stateless를 하게 되면 어떤 점을 신경을 써야 하는지 알아보자.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Statless 구조는 서버가 상태를 가지지 않기 때문에 확장성과 유연성 측면에서 큰 장점을 가진다.&lt;br /&gt;하지만 상태를 외부로 분리한 만큼 이로 인해 새롬게 고려해야 할 문제들도 생기게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 네트워크 비용 증가이다.&lt;br /&gt;Stateless 환경에서는 요청마다 필요한 정보를 모두 포함해야 한다.&lt;br /&gt;예를 들어, JWT를 사용하는 경우, 매 요청마다 토큰이 함께 전달된다. 또한 필요한 데이터를 조회하기 위해 DB나 Redis와 같은 외부 저장소를 호출해야 한다. 이로 인해 네트워크 트래픽이 증가할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;424&quot; data-start=&quot;235&quot; data-ke-size=&quot;size16&quot;&gt;두 번째는 데이터 일관성 문제이다.&lt;br /&gt;상태가 여러 저장소에 분산되면서 데이터가 즉시 반영되지 않을 수 있다. 특히 분산 캐시나 복제된 데이터베이스를 사용하는 경우, 어느 시점에서는 다른 데이터를 바라보는 상황이 발생할 수 있다. 이를 궁극적 일관성이라고 한다.&lt;/p&gt;
&lt;p data-end=&quot;424&quot; data-start=&quot;235&quot; data-ke-size=&quot;size16&quot;&gt;세 번째는 멱등성이다.&lt;br /&gt;Stateless 환경에서는 동일한 요청이 여러 번 들어올 수 있다. 예를 들어 네트워크 문제로 인해 요청이 재시도되는 경우, 같은 요청이 중복 실행이 될 수 있다. 이때 요청을 여러 번 처리하더라도 결과가 동일하게 설계해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;424&quot; data-start=&quot;235&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;네 번째는 보안 문제이다.&lt;br /&gt;JWT와 같은 토큰 기반 인증을 사용할 경우, 토큰이 탈취되면, 만료 전까지는 계속 사용할 수 있다.&amp;nbsp;&lt;br /&gt;세션 기반 방식처럼 서버에서 즉시 무효화하기 어렵기 때문에 토큰의 만료 시간이나 재발급 전략을 신중하게 설계해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;424&quot; data-start=&quot;235&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 운영 복잡성 증가이다.&lt;br /&gt;서버는 단순하지만, 대신 DB, Redis, 메시지 큐 등 여러 외부 시스템을 함께 운영해야 한다. 이로 인해 전체 시스템의 복잡도가 증가하고 문제 발생 시 디버깅 난이도가 높아질 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;424&quot; data-start=&quot;235&quot; data-ke-size=&quot;size16&quot;&gt;출처&lt;br /&gt;&lt;a href=&quot;https://blog.bytebytego.com/p/stateless-architecture-benefits-and&quot;&gt;https://blog.bytebytego.com/p/stateless-architecture-benefits-and&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/554</guid>
      <comments>https://b-programmer.tistory.com/554#entry554comment</comments>
      <pubDate>Thu, 19 Mar 2026 22:08:06 +0900</pubDate>
    </item>
    <item>
      <title>스프링 이벤트 리뉴얼(1) - Publisher</title>
      <link>https://b-programmer.tistory.com/553</link>
      <description>&lt;figure id=&quot;og_1773831429812&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;스프링 이벤트&quot; data-og-description=&quot;스프링 이벤트(Spring Events)는 애플리케이션 내에서 컴포넌트 간의 느슨한 결합(loose coupling)을 유지하면서 비동기적 또는 동기적으로 메시지를 전달하는 메커니즘입니다.스프링의 Observer 패턴 기&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/394&quot; data-og-url=&quot;https://b-programmer.tistory.com/394&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fvTcJ/dJMb9g5aakB/8OH2cuweQtigRorKavTNjK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/GPc9L/dJMb9iIF6n5/KV87ski594kFuYuDjoJCI0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b31lX3/dJMb9bv1ijX/AZFTy3niIriL1bNabLl8Hk/img.jpg?width=383&amp;amp;height=248&amp;amp;face=108_72_174_144&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/394&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/394&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fvTcJ/dJMb9g5aakB/8OH2cuweQtigRorKavTNjK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/GPc9L/dJMb9iIF6n5/KV87ski594kFuYuDjoJCI0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b31lX3/dJMb9bv1ijX/AZFTy3niIriL1bNabLl8Hk/img.jpg?width=383&amp;amp;height=248&amp;amp;face=108_72_174_144');&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;스프링 이벤트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스프링 이벤트(Spring Events)는 애플리케이션 내에서 컴포넌트 간의 느슨한 결합(loose coupling)을 유지하면서 비동기적 또는 동기적으로 메시지를 전달하는 메커니즘입니다.스프링의 Observer 패턴 기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 스프링 이벤트를 학습한 적이 있습니다. 당시에는 이벤트가 어떻게 전달되고 실행되는지, 즉 내부 동작 방식에 집중해서 이해하려고 했습니다. 이번에는 조금 다른 관점에서 스프링 이벤트를 다시 바라보려고 합니다. 스프링 이벤트는 이벤트를 발행하는 퍼블리셔와 이를 처리하는 리스너로 구성되어 있으며, 이러한 구조는 메시지를 주고받는 MQ와 유사한 형태를 가지고 있습니다. 다만 스프링 이벤트는 Kafka나 RabbitMQ와 같은 외부 메시지 브로커를 사용하는 방식이 아니라, 동일한 애플리케이션 내부, 즉 하나의 JVM 자원을 공유하며 동작한다는 점에서 차이가 있습니다. 이번 글에서는 이러한 특징에 초점을 맞춰, 스프링 이벤트를 단순한 기능 설명이 아닌 애플리케이션 내부 메시지 전달 구조라는 관점에서 다시 정리해보려고 합니다.&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-end=&quot;181&quot; data-start=&quot;58&quot; data-ke-size=&quot;size16&quot;&gt;특정 로직에서 이벤트를 발행하면, 이를 구독하고 있는 리스너가 해당 이벤트를 받아 처리하는 구조로 동작합니다. 이 과정에서 발행자와 처리 로직이 직접적으로 연결되지 않기 때문에, 서로 간의 의존성을 낮출 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;332&quot; data-start=&quot;183&quot; data-ke-size=&quot;size16&quot;&gt;즉, 스프링 이벤트는 하나의 기능을 여러 책임으로 나누고, 이를 느슨하게 연결하기 위한 방식입니다.&lt;br /&gt;예를 들어 회원 가입이 완료된 이후, 포인트 지급이나 알림 발송과 같은 후속 작업을 이벤트로 분리하면, 기존 로직을 수정하지 않고도 기능을 확장할 수 있습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;581&quot; data-start=&quot;491&quot; data-ke-size=&quot;size26&quot;&gt;Publisher&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIHNJh/dJMcaaq3XnM/eZjwCYJw6H5QGvIp20APi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIHNJh/dJMcaaq3XnM/eZjwCYJw6H5QGvIp20APi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIHNJh/dJMcaaq3XnM/eZjwCYJw6H5QGvIp20APi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIHNJh%2FdJMcaaq3XnM%2FeZjwCYJw6H5QGvIp20APi0%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;520&quot; height=&quot;199&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 publisher가 있습니다. String 메시지를 보내는 간단한 코드입니다. 그리고나서 동일한 이벤트 DTO를 사용하는 리스너로 이동하게 되어집니다. 그러면 어떻게 전달 할 수 있는걸까요? 마법이라도 부린 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationEventPublisher부터 이해하도록 하겠습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;다음과 같은 동작 방식을 사용이 되어진다고 합니다.&lt;br /&gt;&lt;/span&gt;publishEvent를 호출하게 되면 ApplicationContext로 전달이 된다고 합니다.&lt;br /&gt;&lt;b&gt;ApplicationContext:모든 객체(Bean)를 생성하고 관리하는 컨테이너&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;1144&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceYilp/dJMcag5SSz7/IsmHqSLAvwzKqDfGsMSFbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceYilp/dJMcag5SSz7/IsmHqSLAvwzKqDfGsMSFbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceYilp/dJMcag5SSz7/IsmHqSLAvwzKqDfGsMSFbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceYilp%2FdJMcag5SSz7%2FIsmHqSLAvwzKqDfGsMSFbk%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;649&quot; height=&quot;237&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인을 해보니 AbstractApplicationContext쪽으로 이동하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1612&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PNVGK/dJMcagdKCrp/Z22Xf7pg6rEbyuT0rdJvOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PNVGK/dJMcagdKCrp/Z22Xf7pg6rEbyuT0rdJvOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PNVGK/dJMcagdKCrp/Z22Xf7pg6rEbyuT0rdJvOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPNVGK%2FdJMcagdKCrp%2FZ22Xf7pg6rEbyuT0rdJvOk%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;420&quot; data-origin-width=&quot;1612&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Event는 아쉽게도? 스프링 객체가 아닙니다. 여기서 드는 의문이 있습니다. 그러면 ApplicationContext에 넘겨주지 않아도 되지 않나 싶습니다. 조금더 정확하게 말하면, 넘겨주는 것이 아니라 발행하는것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비유를 하자면, 우체통에 편지를 넣는다고 해서 편지가 우체국 직원이 되는 건 아닙니다. 편지는 그냥 전달할 내용이고,&lt;br /&gt;우체국 시스템이 그 편지를 적절한 곳으로 보내는 것입니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그렇다는 이야기는 ApplicationContext가 우체통이고, event는 편지라고 할 수 있겠네요. &lt;/span&gt;코드를 자세히 보면 event자체가 ApplicationEvent도 될 수 있다는 사실을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 그전에는 event객체를 만들기 위해서는 AplicationEvent를 상속을 받아 이벤트를 만들었습니다. 하지만 지금의 방식은 dto객체를 전달할 뿐입니다.&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;1414&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HBSod/dJMcacPWyiN/5BZpnIV1HHwOacZUzecGyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HBSod/dJMcacPWyiN/5BZpnIV1HHwOacZUzecGyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HBSod/dJMcacPWyiN/5BZpnIV1HHwOacZUzecGyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHBSod%2FdJMcacPWyiN%2F5BZpnIV1HHwOacZUzecGyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;710&quot; height=&quot;227&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해보면 역시 PayloadApplicationEvent로 applicationEvent를 만들어주고 있습니다.&lt;br /&gt;그위 코드는 typeHint가 명시가 되었을 경우에 사용이 되어집니다. 고로 저희는 다음 코드를 읽을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lqeUE/dJMcaiJlIFO/bXfnAOpgF6xkiSKucRdKIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lqeUE/dJMcaiJlIFO/bXfnAOpgF6xkiSKucRdKIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lqeUE/dJMcaiJlIFO/bXfnAOpgF6xkiSKucRdKIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlqeUE%2FdJMcaiJlIFO%2FbXfnAOpgF6xkiSKucRdKIk%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;736&quot; height=&quot;178&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 typeHint.. eventType 모두 null인 상태입니다. 고로 타입을 확인을 해줘야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mi5Nf/dJMcai3Dnv3/kSYKmwzIIVM3AcszV1yzpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mi5Nf/dJMcai3Dnv3/kSYKmwzIIVM3AcszV1yzpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mi5Nf/dJMcai3Dnv3/kSYKmwzIIVM3AcszV1yzpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmi5Nf%2FdJMcai3Dnv3%2FkSYKmwzIIVM3AcszV1yzpk%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;740&quot; height=&quot;241&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 ResolvableTypeProvider를 구현하여 타입 정보를 직접 제공할 수 있다면 해당 타입을 사용하고, 그렇지 않다면 객체의 런타임 클래스 타입을 기반으로 판단합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1592&quot; data-origin-height=&quot;820&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBdeqr/dJMcagEPA5f/1fA92GuTZec9s5ttKVZL3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBdeqr/dJMcagEPA5f/1fA92GuTZec9s5ttKVZL3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBdeqr/dJMcagEPA5f/1fA92GuTZec9s5ttKVZL3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBdeqr%2FdJMcagEPA5f%2F1fA92GuTZec9s5ttKVZL3K%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;718&quot; height=&quot;370&quot; data-origin-width=&quot;1592&quot; data-origin-height=&quot;820&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약간의아하는 곳이 있다고 여겨집니다. 바로 earlyApplicationEvents입니다. 이는 스프링의 초기화여부에 따라 결정이 되어집니다.&lt;br /&gt;즉, 스프링 초기화가 이미 진행중이라면, null이 아니기 때문에 earlyApplicationEvents에 등록되어집니다. 그리고 나서 초기화가 완료가 되어지면 그제서야 이벤트를 발행이 되어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 일반적으로 springEvent를 사용할때는 필요가 없습니다. 왜냐하면 스프링 이벤트는 단순히 메시지(payload)만 발행하는 것이 아닙니다. 기본적으로 제공되어지는 스프링 이벤트들도 굉장히 많습니다. 예를 들면, 스프링 준비가 완료가 되었다, 스프링 환경 설정이 종료되었다는 등을 뜻합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 그게 아니라면 applicationEventMulticaster를 통해 메시지를 발행이 되어진다는 것을 확인 할 수 있습니다.&lt;br /&gt;자세히보면 eventType을 함께 전달 하고 있습니다. 이는 같은 eventType에게 모두 발행이 되어진다는 뜻이 되어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 하위에 parent도 사실은 ApplicationContext입니다. 이말 뜻은 스프링에서 관리가 되어지는 객체라는 뜻입니다. 고로 이것 또한 스프링이 내부적으로 만든 이벤트들을 위해 만들어졌다고 생각하면 될거 같습니다. 간단하게 생각했을때, 자식이 하는 일을 부모도 알게 한다고 이해면 될거 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ApplicationEventMulticaster&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1614&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMf8JJ/dJMcacWGUi7/ihUMWTlMgKJKNrmKZEQ8t1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMf8JJ/dJMcacWGUi7/ihUMWTlMgKJKNrmKZEQ8t1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMf8JJ/dJMcacWGUi7/ihUMWTlMgKJKNrmKZEQ8t1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMf8JJ%2FdJMcacWGUi7%2FihUMWTlMgKJKNrmKZEQ8t1%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;1614&quot; height=&quot;848&quot; data-origin-width=&quot;1614&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;691&quot; data-start=&quot;598&quot; data-ke-size=&quot;size16&quot;&gt;이제 거의 마지막 단계입니다.&lt;br /&gt;단순히 publisher 하나의 흐름을 따라왔을 뿐인데, 생각보다 내부 구조가 길고 세분화되어 있다는 점을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;865&quot; data-start=&quot;693&quot; data-ke-size=&quot;size16&quot;&gt;ApplicationEventMulticaster는 먼저 현재 이벤트와 타입 정보에 맞는 ApplicationListener들을 조회합니다.&lt;br /&gt;이 과정에서 어떤 리스너가 해당 이벤트를 처리할 수 있는지 선별하게 되며, 내부적으로는 추가적인 필터링 로직이 존재하지만 여기서는 자세히 다루지 않겠습니다. 이후 실제 리스너 실행 단계로 넘어갑니다.&lt;br /&gt;비동기 방식이라면 Executor를 통해 invokeListener를 실행하고, 동기 방식이라면 현재 호출 흐름에서 직접 invokeListener를 수행합니다.&lt;/p&gt;
&lt;p data-end=&quot;1111&quot; data-start=&quot;991&quot; data-ke-size=&quot;size16&quot;&gt;결국 이 단계의 핵심 역할은 명확합니다.&lt;br /&gt;ApplicationEventMulticaster는 이벤트를 처리할 리스너를 찾고, 동기 또는 비동기 방식에 맞춰 해당 리스너에게 이벤트를 전달하는 역할을 수행합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;기존에는 listener까지 학습을 진행하려고 했지만, 생각보다 길게 된느낌이 듭니다. 그래서 publisher까지만 작성하고 listener쪽은 다음에 진행하려고 합니다. instener을 학습할때는, 어노테이션별로 어떻게 동작이 되어지는지 이해해보는 과정을 가져보면 좋다고 느껴집니다.&lt;br /&gt;특히 트랜잭션리스너와 일반 리스너의 차이에 대해 학습할 예정입니다.&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/553</guid>
      <comments>https://b-programmer.tistory.com/553#entry553comment</comments>
      <pubDate>Wed, 18 Mar 2026 23:08:15 +0900</pubDate>
    </item>
    <item>
      <title>라이브락 vs 데드락</title>
      <link>https://b-programmer.tistory.com/552</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 데드락 관련 질문을 한적이 있었습니다. 그 당시 락을 학습을 했었을 때였던걸로 기억합니다. 아무튼 어떤 질문을 했었는데 그거는 데드락이 아니고 라이브락이라는 답변을 받은 적이 있습니다. 지금 이것들이 무엇인지 어떤건지 답변하기 어려워 이번 챕터를 준비를 하였습니다. 라이브락과 데드락의 차이는 무엇일까요?&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;178&quot; data-start=&quot;157&quot; data-section-id=&quot;1w1s3wn&quot; data-ke-size=&quot;size26&quot;&gt;데드락 (Deadlock)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deadlock은 두 단어로 이루어져 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;178&quot; data-start=&quot;136&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;157&quot; data-start=&quot;136&quot; data-section-id=&quot;bj497b&quot;&gt;Dead : 죽은, 멈춘&lt;/li&gt;
&lt;li data-end=&quot;178&quot; data-start=&quot;158&quot; data-section-id=&quot;kvdka9&quot;&gt;Lock : 잠금, 자물쇠&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;232&quot; data-start=&quot;180&quot; data-ke-size=&quot;size16&quot;&gt;이를 직역하면 &quot;죽은 잠금&quot;, 혹은 &quot;완전히 막혀버린 상태&quot;라는 의미가 됩니다.&lt;br /&gt;여기서 중요한 것은 Dead입니다. 단순히 잠겨 있는 상태가 아니라 더 이상 아무것도 진행되지 않는 상태를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 유명한 예시가 식사하는 철학자 문제입니다. 여러 명의 철학자가 원형 테이블에 앉아 식사를 하고 있습니다.&lt;br /&gt;각 철학자 사이에는 포크가 하나씩 놓여 있습니다. 철학자는 식사를 하기 위해 양쪽 포크 두 개가 필요합니다.&lt;br /&gt;예를 들어 철학자 A가 식사를 하려면 왼쪽 포크, 오른쪽 포크두 개를 모두 들어야 합니다.&lt;br /&gt;그런데 모든 철학자가 동시에 왼쪽 포크를 먼저 집었다고 가정해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GP0eX/dJMcai3By8F/O4IgI6eChks8WsbtxocZqk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GP0eX/dJMcai3By8F/O4IgI6eChks8WsbtxocZqk/img.webp&quot; data-alt=&quot;출처: 나무위키&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GP0eX/dJMcai3By8F/O4IgI6eChks8WsbtxocZqk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGP0eX%2FdJMcai3By8F%2FO4IgI6eChks8WsbtxocZqk%2Fimg.webp&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;330&quot; height=&quot;332&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;332&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: 나무위키&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;363&quot; data-start=&quot;349&quot; data-ke-size=&quot;size16&quot;&gt;그 결과는 어떻게 될까요?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;460&quot; data-start=&quot;365&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;388&quot; data-start=&quot;365&quot; data-section-id=&quot;1ecjewy&quot;&gt;철학자 A는 오른쪽 포크를 기다립니다.&lt;/li&gt;
&lt;li data-end=&quot;412&quot; data-start=&quot;389&quot; data-section-id=&quot;9lk1xt&quot;&gt;철학자 B도 오른쪽 포크를 기다립니다.&lt;/li&gt;
&lt;li data-end=&quot;436&quot; data-start=&quot;413&quot; data-section-id=&quot;1roy7tc&quot;&gt;철학자 C도 오른쪽 포크를 기다립니다.&lt;/li&gt;
&lt;li data-end=&quot;460&quot; data-start=&quot;437&quot; data-section-id=&quot;7wl51z&quot;&gt;철학자 D도 오른쪽 포크를 기다립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;499&quot; data-start=&quot;462&quot; data-ke-size=&quot;size16&quot;&gt;하지만 그 오른쪽 포크는 이미 다른 철학자가 잡고 있습니다. 결국 모든 철학자는 다음과 같은 상태가 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;560&quot; data-start=&quot;529&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;544&quot; data-start=&quot;529&quot; data-section-id=&quot;x9q11d&quot;&gt;하나의 포크는 잡고 있음&lt;/li&gt;
&lt;li data-end=&quot;560&quot; data-start=&quot;545&quot; data-section-id=&quot;7uc9ed&quot;&gt;다른 포크를 기다리는 중&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;588&quot; data-start=&quot;562&quot; data-ke-size=&quot;size16&quot;&gt;하지만 그 포크는 절대 풀리지 않습니다. 그래서 아무도 식사를 하지 못한 채 영원히 기다리게 됩니다.&lt;br /&gt;이 상태가 바로 데드락(Deadlock) 입니다. 즉, &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;서로가 가진 자원을 기다리면서 &lt;/span&gt;아무도 작업을 진행하지 못하는 상태입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 철학자는 스레드 혹은 트랜잭션일겁니다. 그리고 포크는 공유 자원입니다. 어떻게 보면 되게 탐욕스럽다고 할 수 있죠.&lt;br /&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;800&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brKcb8/dJMcacbi5P2/ZNMpC7NG6vDwv6EVkP3byK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brKcb8/dJMcacbi5P2/ZNMpC7NG6vDwv6EVkP3byK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brKcb8/dJMcacbi5P2/ZNMpC7NG6vDwv6EVkP3byK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrKcb8%2FdJMcacbi5P2%2FZNMpC7NG6vDwv6EVkP3byK%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;406&quot; height=&quot;325&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 데드락이 언제나 발생할까요? 데드락이 발생하는 원인은 총 4가지로 설명할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 상호 배제 (Mutual Exclusion)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 같은 자원을 사용하는 것은 불가능합니다. 다시 철학자 문제를 생각해봅시다.&amp;nbsp;&lt;br /&gt;철학자문제에서 2개의 포크를 집어야 식사가 가능했다고 하였습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;1144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/snA5R/dJMcafePYZR/zBvkS4E0elw57zMR7JHh0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/snA5R/dJMcafePYZR/zBvkS4E0elw57zMR7JHh0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/snA5R/dJMcafePYZR/zBvkS4E0elw57zMR7JHh0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsnA5R%2FdJMcafePYZR%2FzBvkS4E0elw57zMR7JHh0K%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;381&quot; height=&quot;334&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;1144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번철학자와 2번 철학자는 동시에 식사를 할 수 없습니다. 왜냐하면 이미 포크를 이미 누군가(1 or2)가 사용하고 있기 때문이죠.&lt;br /&gt;이를 방지하기 위해서는 순차적으로 식사를 하는 방법을 사용해야 할거 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 점유 대기 (Hold and Wait)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점유 대기(Hold and Wait)는 이미 하나의 자원을 점유한 상태에서 다른 자원을 기다리는 상황을 의미합니다.&lt;br /&gt;다시 식사하는 철학자 문제로 돌아가 보겠습니다. 철학자가 식사를 하기 위해서는 왼쪽 포크와 오른쪽 포크 두 개가 필요합니다.&lt;br /&gt;예를 들어 1번 철학자가 다음과 같은 상황이라고 가정해 보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;521&quot; data-start=&quot;472&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;490&quot; data-start=&quot;472&quot; data-section-id=&quot;q020vz&quot;&gt;왼쪽 포크는 이미 집었습니다.&lt;/li&gt;
&lt;li data-end=&quot;521&quot; data-start=&quot;491&quot; data-section-id=&quot;1ggxl73&quot;&gt;하지만 오른쪽 포크는 다른 철학자가 사용 중입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;544&quot; data-start=&quot;523&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 1번 철학자는 어떻게 될까요?&lt;br /&gt;왼쪽 포크는 계속 쥔 상태로 유지하면서, 오른쪽 포크가 사용 가능해질 때까지 기다리게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;612&quot; data-start=&quot;610&quot; data-ke-size=&quot;size16&quot;&gt;즉, 하나의 자원은 점유하고 있고 다른 자원은 대기하고 있는 상태 이것이 바로 점유 대기(Hold and Wait) 입니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;여기서 중요한 점은 &lt;/span&gt;자원을 놓지 않는다는 것&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;입니다. &lt;br /&gt;&lt;/span&gt;만약 철학자가 다음과 같이 행동한다면 상황은 달라집니다.&lt;br /&gt;오른쪽 포크가 없으면 왼쪽 포크도 내려놓는다 이렇게 되면 점유 대기 조건이 깨지게 됩니다.&lt;br /&gt;즉, &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이미 가진 자원을 계속 쥐고 있으면서 다른 자원을 기다리기 때문에 &lt;/span&gt;데드락이 발생할 수 있는 환경이 만들어집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 비선점 (No Preemption)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비선점(No Preemption)은 이미 점유된 자원을 강제로 빼앗을 수 없다는 특징을 의미합니다.&lt;br /&gt;다시 식사하는 철학자 문제를 예로 들어보겠습니다.&lt;br /&gt;철학자가 포크를 하나 집었다면, 다른 철학자가 그 포크를 강제로 빼앗을 수 없습니다.&lt;br /&gt;즉, 포크를 집은 철학자가 스스로 내려놓기 전까지는 다른 철학자가 사용할 수 없습니다.&lt;br /&gt;이처럼 자원을 강제로 회수할 수 없다면, 자원을 기다리는 상황이 더 쉽게 만들어질 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 순환 대기 (Circular Wait)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순환 대기(Circular Wait)는 여러 작업들이 서로의 자원을 기다리는 순환 구조가 만들어지는 상황을 의미합니다.&lt;br /&gt;예를 들어 다음과 같은 상황을 생각해 볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1773667781830&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;철학자 A &amp;rarr; 철학자 B의 포크 대기
철학자 B &amp;rarr; 철학자 C의 포크 대기
철학자 C &amp;rarr; 철학자 D의 포크 대기
철학자 D &amp;rarr; 철학자 A의 포크 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 서로가 서로의 자원을 기다리는 순환 구조가 만들어지면 어느 누구도 작업을 진행할 수 없게 됩니다.&lt;br /&gt;이 상태가 바로 데드락입니다.&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이렇게 4가지 원인.. 아니고 특징에 대해 작성해봤는데 어찌보면 당연한느낌이라고 인거 같습니다. 즉, 4가지가 충족이되어지면, 데드락이 발생하는 것이 아니라 데드락이 발생이 되어지면 4가지의 조건이 있다고 여겨집니다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;607&quot; data-start=&quot;585&quot; data-section-id=&quot;1a3tn6m&quot; data-ke-size=&quot;size26&quot;&gt;라이브락 (Livelock)&lt;/h2&gt;
&lt;div&gt;
&lt;p data-end=&quot;164&quot; data-start=&quot;147&quot; data-ke-size=&quot;size16&quot;&gt;역시 어원을 먼저 생각해봅시다.&lt;/p&gt;
&lt;p data-end=&quot;192&quot; data-start=&quot;166&quot; data-ke-size=&quot;size16&quot;&gt;Livelock = Live + Lock&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;238&quot; data-start=&quot;194&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;222&quot; data-start=&quot;194&quot; data-section-id=&quot;1bqaa24&quot;&gt;Live : 살아있는, 계속 움직이는&lt;/li&gt;
&lt;li data-end=&quot;238&quot; data-start=&quot;223&quot; data-section-id=&quot;1sbiahd&quot;&gt;Lock : 잠금&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;269&quot; data-start=&quot;240&quot; data-ke-size=&quot;size16&quot;&gt;직역해 보면 살아있는 락이라는 의미입니다.&lt;br /&gt;이 말은 조금 이상하게 들릴 수도 있습니다. 왜냐하면 락이 걸렸다면 보통 멈춰 있다고 생각하기 때문입니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하지만 라이브락은 조금 다른 특징을 가지고 있습니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;스레드나 작업이 &lt;/span&gt;멈춰 있는 것은 아니지만&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;, &lt;/span&gt;서로의 행동 때문에 작업이 계속 진행되지 않는 상태입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 보면, 데드락과 정반대의 길을 걷는다고 할 수 있습니다. 이것은 반은 맞고 반은 틀린 표현이라고 생각합니다.&lt;br /&gt;혹자는, 데드락의 반대고 점유의 반대가 양보니까 그 둘을 반대로 생각하는 경우도 있을거라고 생각합니다.&lt;br /&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;br /&gt;위 특징에 대입을 해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 철학자 5명이서 식사를 하고 있습니다. 역시나 양쪽에 포크를 들어야 식사가 가능했습니다.&lt;br /&gt;이전에는 철학자끼리 자신이 먼저 식사를 하려고 하다가 음식이 상한 경험이 있는지 이번에는 서로 양보하기 시작했습니다.&lt;/p&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GP0eX/dJMcai3By8F/O4IgI6eChks8WsbtxocZqk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GP0eX/dJMcai3By8F/O4IgI6eChks8WsbtxocZqk/img.webp&quot; data-alt=&quot;출처: 나무위키&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GP0eX/dJMcai3By8F/O4IgI6eChks8WsbtxocZqk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGP0eX%2FdJMcai3By8F%2FO4IgI6eChks8WsbtxocZqk%2Fimg.webp&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;330&quot; height=&quot;332&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;332&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: 나무위키&lt;/figcaption&gt;
&lt;/figure&gt;
여기서 주목해야 하는 점은 점유를 하려 포크를 봤을때, 이미 사용중이라면, 데드락인 경우에는 기다렸습니다.&lt;br /&gt;하지만 양보는 다릅니다.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양보의 가장 큰 특징은 가만히 앉아서 양보가 불가능하다는 점입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양보는 움직여야 합니다. 아니면 말로 식사를 먼저 권유를 해야겠죠?&lt;br /&gt;하지만 모든 철학자가 배려심이 넘쳐서 양보를 계속한다고 해봅시다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 드세요 먼저 드세요.... 철학자들은 계속 움직이고 양보할겁니다.&lt;br /&gt;하지만 음식은 아무도 먹지 않는 기이한 현상이 발생이 되는 거죠. 이것이 라이브락입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;식사하는 철학자 문제를 통해 데드락과 라이브락에 대해 살펴보았습니다. 데드락은 서로 자원을 기다리면서 아무도 움직이지 못하는 상태이고, 라이브락은 서로 양보하려다 계속 움직이지만 작업이 진행되지 않는 상태입니다. 여기서 기억해야 할 점은 양보를 하려면 움직여야 한다는 점입니다. 움직이지 않으면 양보할 수도 없기 때문에, 결국 아무것도 진행되지 않는 데드락 상태가 됩니다. 반대로 계속 움직이며 서로를 피하려다 보면 라이브락이 발생할 수 있습니다. 결국 두 문제는 방식은 다르지만, 시스템의 진행을 막는다는 점에서는 같은 문제라고 볼 수 있습니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/552</guid>
      <comments>https://b-programmer.tistory.com/552#entry552comment</comments>
      <pubDate>Mon, 16 Mar 2026 23:21:45 +0900</pubDate>
    </item>
    <item>
      <title>문제 발견: LazyConnectionDataSourceProxy 톺아보기</title>
      <link>https://b-programmer.tistory.com/550</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1773384889621&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;read-only일때 replica 사용하게 하기&quot; data-og-description=&quot;master-slave 구조 이해하기DB에서 master-slave 형태로 변경하다는 뜻은 읽기 전용과 쓰기 전용을 분리한다는 의미입니다.최근에는 master-slave라는 용어보다는 primary-replica라는 용어로 더 많이 사용이 &quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/528?utm_source=chatgpt.com&quot; data-og-url=&quot;https://b-programmer.tistory.com/528&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cgZHoF/dJMb9iaPEsl/8GcHIIkbv0wt7lVh9KcuZ1/img.png?width=800&amp;amp;height=368&amp;amp;face=0_0_800_368,https://scrap.kakaocdn.net/dn/ca5b3j/dJMb9c9wm8F/utIyVxjlRZiMQcAejHAzhk/img.png?width=800&amp;amp;height=368&amp;amp;face=0_0_800_368,https://scrap.kakaocdn.net/dn/MQD4g/dJMb9bv0OyQ/J3d8Qto26nCBOeC5rd8460/img.png?width=1670&amp;amp;height=770&amp;amp;face=0_0_1670_770&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/528?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/528?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cgZHoF/dJMb9iaPEsl/8GcHIIkbv0wt7lVh9KcuZ1/img.png?width=800&amp;amp;height=368&amp;amp;face=0_0_800_368,https://scrap.kakaocdn.net/dn/ca5b3j/dJMb9c9wm8F/utIyVxjlRZiMQcAejHAzhk/img.png?width=800&amp;amp;height=368&amp;amp;face=0_0_800_368,https://scrap.kakaocdn.net/dn/MQD4g/dJMb9bv0OyQ/J3d8Qto26nCBOeC5rd8460/img.png?width=1670&amp;amp;height=770&amp;amp;face=0_0_1670_770');&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;read-only일때 replica 사용하게 하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;master-slave 구조 이해하기DB에서 master-slave 형태로 변경하다는 뜻은 읽기 전용과 쓰기 전용을 분리한다는 의미입니다.최근에는 master-slave라는 용어보다는 primary-replica라는 용어로 더 많이 사용이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Master-Slave 환경을 구성하던 중 이상한 현상을 발견했습니다. 정상적인 구조라면 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Transactional(readOnly = true)&lt;/span&gt; 상황에서는&lt;br /&gt;Primary가 아니라 Replica로 라우팅되어야 합니다. 하지만 테스트를 진행해보니 예상과 다르게 ReadOnly 트랜잭션에서도 Primary가 사용되는 상황이 발생했습니다. 코드를 잘못 작성한 것일까 여러 부분을 확인해보았지만 특별히 문제를 발견하지는 못했습니다. 그러던 중 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;LazyConnectionDataSourceProxy&lt;/span&gt;를 사용하면 이 문제가 해결된다는 것을 발견했습니다. 실제로 적용해보니 ReadOnly 트랜잭션에서 정상적으로 Replica로 라우팅되었습니다. 하지만 여기서 한 가지 의문이 들었습니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;LazyConnectionDataSourceProxy&lt;/span&gt;를 하나 추가했을 뿐인데 &lt;/span&gt;왜 문제가 해결된 것일까요? 단순히 &lt;b&gt;붙였더니 된다&lt;/b&gt;&amp;nbsp;수준으로 넘어가기에는 이 동작의 원인을 정확히 이해하지 못한 상태였습니다. 그래서 이번 글에서는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;LazyConnectionDataSourceProxy&lt;/span&gt;가 무엇인지, 그리고 왜 이 문제가 발생했고 어떻게 해결되는지를 제대로 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;LazyConnectionDataSourceProxy란 무엇일까?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;간단히 말해 LazyConnectionDataSourceProxy는 커넥션 시점을 늦추는 역할을 합니다.&lt;br /&gt;&lt;/span&gt;이 문제를 이해하기 위해서는 먼저 스프링이 언제 데이터베이스 커넥션을 획득하는지 살펴볼 필요가 있습니다.&lt;br /&gt;트랜잭션이 새롭게 시작될 때마다 데이터베이스와 통신하기 위해 Connection을 하나 획득하게 됩니다.&lt;br /&gt;다음과 같은 코드가 존재한다고 합시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GNIH4/dJMcai3zu85/qyebCJRzTpTK8QWVFBooJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GNIH4/dJMcai3zu85/qyebCJRzTpTK8QWVFBooJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GNIH4/dJMcai3zu85/qyebCJRzTpTK8QWVFBooJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGNIH4%2FdJMcai3zu85%2FqyebCJRzTpTK8QWVFBooJ0%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;642&quot; height=&quot;379&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;730&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 게시글을 수정하는 간단한 코드입니다. 스프링에서 @Transactional이 적용된 메서드가 호출되면 TransactionManager가 실행되며 트랜잭션을 시작합니다. 이 과정에서 DataSource로부터 Connection을 하나 획득하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 주목해야 하는 부분은 &lt;br /&gt;1. TransactionManager가 트랜잭션이 실행이되며 트랜잭션이 시작이 된다.&amp;nbsp;&lt;br /&gt;2. 그 과정에서 DataSource가 어떻게 Connection을 획득하게 하는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 보면 TransacinalManager이 트랜잭션을 얻는 방법에 대해 나와있습니다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1773391178303&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;어떻게 트랜잭셕은 Read only 여부를 알 수 있을까?&quot; data-og-description=&quot;read-only일때 replica 사용하게 하기master-slave 구조 이해하기DB에서 master-slave 형태로 변경하다는 뜻은 읽기 전용과 쓰기 전용을 분리한다는 의미입니다.최근에는 master-slave라는 용어보다는 primary-repl&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/535?utm_source=chatgpt.com&quot; data-og-url=&quot;https://b-programmer.tistory.com/535&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jXfEQ/dJMb8QMaulK/OcoXkg8NNpnbk7qnnVlN20/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/rTjot/dJMb8SpGs9L/z2D4z1ODQNogoCW5oETox1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/mu02p/dJMb8Rj0rIn/hGeTGFuchidb98GsWdXM01/img.png?width=1684&amp;amp;height=1050&amp;amp;face=0_0_1684_1050&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/535?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/535?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jXfEQ/dJMb8QMaulK/OcoXkg8NNpnbk7qnnVlN20/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/rTjot/dJMb8SpGs9L/z2D4z1ODQNogoCW5oETox1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/mu02p/dJMb8Rj0rIn/hGeTGFuchidb98GsWdXM01/img.png?width=1684&amp;amp;height=1050&amp;amp;face=0_0_1684_1050');&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;어떻게 트랜잭셕은 Read only 여부를 알 수 있을까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;read-only일때 replica 사용하게 하기master-slave 구조 이해하기DB에서 master-slave 형태로 변경하다는 뜻은 읽기 전용과 쓰기 전용을 분리한다는 의미입니다.최근에는 master-slave라는 용어보다는 primary-repl&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 복습해보면 PlatformTransactionManager -&amp;gt; AbstractPlatformTransactionManager으로 트랜잭션을 전달하고 있는것을 확인 할 수 있습니다.&amp;nbsp;&lt;br /&gt;AbstractPlatformTransactionManager는 PlatformTransactionManager을 상속받고 있는 클래스입니다. 즉, 자식 클래스라는 뜻이죠. &lt;br /&gt;트랜잭션이 시작이 되어질때, getTransaction을 얻게 되어집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XlS8L/dJMcaaEyfuL/2IF6oK6Zg59uYOYtZXsABK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XlS8L/dJMcaaEyfuL/2IF6oK6Zg59uYOYtZXsABK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XlS8L/dJMcaaEyfuL/2IF6oK6Zg59uYOYtZXsABK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXlS8L%2FdJMcaaEyfuL%2F2IF6oK6Zg59uYOYtZXsABK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;682&quot; height=&quot;379&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 시작이 된다는것을 알 수 있습니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;getTransaction() 단계에서 기존 트랜잭션의 존재 여부를 확인합니다.&lt;br /&gt;&lt;/span&gt;이후 새로운 트랜잭션이 필요하다고 판단되면 doBegin()이 호출되며, 이 과정에서 실제 Connection을 획득하고 트랜잭션이 시작됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1756&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pb5Zt/dJMcaiWPbLL/ohFXLbVJuPsBPx4CF7AHTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pb5Zt/dJMcaiWPbLL/ohFXLbVJuPsBPx4CF7AHTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pb5Zt/dJMcaiWPbLL/ohFXLbVJuPsBPx4CF7AHTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPb5Zt%2FdJMcaiWPbLL%2FohFXLbVJuPsBPx4CF7AHTK%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;1756&quot; height=&quot;328&quot; data-origin-width=&quot;1756&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름에서 알 수 있듯이 AbstractPlatformTransactionManager는 추상 클래스입니다.&lt;br /&gt;따라서 트랜잭션을 실제로 시작하는 doBegin()의 구체적인 동작은 하위 구현체에서 처리하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6TNGf/dJMcagxXprV/IAugYVC27trAaTqvYXOj21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6TNGf/dJMcagxXprV/IAugYVC27trAaTqvYXOj21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6TNGf/dJMcagxXprV/IAugYVC27trAaTqvYXOj21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6TNGf%2FdJMcagxXprV%2FIAugYVC27trAaTqvYXOj21%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;2048&quot; height=&quot;402&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataSourceTransctionanManager을 선택하면,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRkG4P/dJMcacI8qzJ/if9Au59DNgllqpzDy4C3F1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRkG4P/dJMcacI8qzJ/if9Au59DNgllqpzDy4C3F1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRkG4P/dJMcacI8qzJ/if9Au59DNgllqpzDy4C3F1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRkG4P%2FdJMcacI8qzJ%2Fif9Au59DNgllqpzDy4C3F1%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;739&quot; height=&quot;293&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection이 획득된다는것을 확인 할 수 있습니다. 그렇다면 LazyConnectionDataSourceProxy이라는것은 이름에서 알 수 있듯이 이 프록시는 Connection을 획득하는 시점을 지연시키는 역할을 합니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Connection획득시점을 지연을 시키는 이유?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 상황을 다시 살펴보겠습니다.&lt;br /&gt;트랜잭션이 ReadOnly인 상황에서 REPLICA가 아닌 PRIMARY로 동작이 되어지는 상황입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7XNWf/dJMcaiWPecp/BJcfwT8GLdvAN9siaeufG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7XNWf/dJMcaiWPecp/BJcfwT8GLdvAN9siaeufG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7XNWf/dJMcaiWPecp/BJcfwT8GLdvAN9siaeufG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7XNWf%2FdJMcaiWPecp%2FBJcfwT8GLdvAN9siaeufG1%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;534&quot; height=&quot;260&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 Connection이 획득되는 내부 과정은 잠시 제외하고, 위 흐름만 기준으로 생각해보겠습니다.&lt;br /&gt;DataSource에서 Connection을 획득하는 시점에 라우팅이 이루어지며, 이때 어떤 데이터베이스를 사용할지 결정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 생각해보면, 원래는 라우팅 이후에 사용할 데이터베이스가 결정되어야 합니다. 하지만 실제로는 라우팅이 수행되기 전에 이미 Connection이 획득되었습니다. 이는 곧 라우팅 로직이 반영되기 전에 데이터베이스 선택이 먼저 이루어졌다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;저희가 해야할일은 라우팅 이후에 커넥션을 받도록 유도를 하는것입니다. 이것을&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;LazyConnectionDataSourceProxy을 통해 커넥션 시점을 뒤로 미루는 이유도 요겁니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LazyConnectionDataSourceProxy의 코드를 확인해봅시다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection을 받는 부분을 살펴봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kEDv6/dJMcahRbiwA/s4LQki19dm1aqSWHqHTAB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kEDv6/dJMcahRbiwA/s4LQki19dm1aqSWHqHTAB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kEDv6/dJMcahRbiwA/s4LQki19dm1aqSWHqHTAB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkEDv6%2FdJMcahRbiwA%2Fs4LQki19dm1aqSWHqHTAB1%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;534&quot; height=&quot;208&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸로 Connection은 프록시로 만들어졌고 다음과 같은 과정을 가지게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773395570035&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LazyConnectionDataSourceProxy -&amp;gt; Connection Proxy -&amp;gt; Statement 실행 시 -&amp;gt; 실제 Connection 획득&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Primary-Replica 환경에서 예상과 다르게 데이터베이스 라우팅이 정상적으로 동작하지 않는 상황을 확인할 수 있었습니다. LazyConnectionDataSourceProxy를 적용하자 해당 문제가 해결되는 것을 확인할 수 있었고, 이를 통해 왜 이 클래스가 필요한지에 대해 살펴보는 시간을 가졌습니다. 결국 문제의 핵심은 Connection을 언제 획득하느냐에 있었으며, LazyConnectionDataSourceProxy는 Connection 획득 시점을 지연시켜 올바른 데이터베이스 라우팅이 이루어지도록 도와주는 역할을 한다는 것을 알 수 있었습니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/550</guid>
      <comments>https://b-programmer.tistory.com/550#entry550comment</comments>
      <pubDate>Fri, 13 Mar 2026 18:58:00 +0900</pubDate>
    </item>
    <item>
      <title>Actuator 메트릭  생성 하기</title>
      <link>https://b-programmer.tistory.com/549</link>
      <description>&lt;figure id=&quot;og_1773227980012&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;Actuator&quot; data-og-description=&quot;k6를 통해 부하 테스트와 API 테스트를 진행하면서 한 가지 한계를 느꼈습니다.요청을 보내고 결과를 확인하는 것은 가능했지만, 실제로 부하가 걸리는 동안 애플리케이션 내부에서 어떤 변화가 &quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/548&quot; data-og-url=&quot;https://b-programmer.tistory.com/548&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/i3h4x/dJMb8T9XOyi/UcpV4qXdNuBTv2jxxXwXqk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/KVE6t/dJMb8RRQedi/Rsk0zbxKMTIpmS6lZQotJ1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cIfv0e/dJMb8XkdHyw/SUjRonzbZM9y9qU1TLRh20/img.png?width=1820&amp;amp;height=592&amp;amp;face=0_0_1820_592&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/548&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/548&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/i3h4x/dJMb8T9XOyi/UcpV4qXdNuBTv2jxxXwXqk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/KVE6t/dJMb8RRQedi/Rsk0zbxKMTIpmS6lZQotJ1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cIfv0e/dJMb8XkdHyw/SUjRonzbZM9y9qU1TLRh20/img.png?width=1820&amp;amp;height=592&amp;amp;face=0_0_1820_592');&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;Actuator&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;k6를 통해 부하 테스트와 API 테스트를 진행하면서 한 가지 한계를 느꼈습니다.요청을 보내고 결과를 확인하는 것은 가능했지만, 실제로 부하가 걸리는 동안 애플리케이션 내부에서 어떤 변화가&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 장에서 Actuator에 대해 학습해봤습니다. Actuator는 Spring Boot 애플리케이션의 상태 정보를 확인할 수 있도록 제공되는 기능이라는 것을 알 수 있었습니다. Actuator를 통해 애플리케이션의 상태뿐만 아니라 메트릭 정보도 확인할 수 있으며, Prometheus와 같은 모니터링 시스템과 연동할 수 있는 endpoint도 제공됩니다. 또한 기본적으로 제공되는 다양한 메트릭 정보를 확인할 수 있다는 점도 알 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;1506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/APAQ9/dJMcach1ulF/HItuwusPVR7ecZsXNmOdYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/APAQ9/dJMcach1ulF/HItuwusPVR7ecZsXNmOdYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/APAQ9/dJMcach1ulF/HItuwusPVR7ecZsXNmOdYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAPAQ9%2FdJMcach1ulF%2FHItuwusPVR7ecZsXNmOdYk%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;251&quot; height=&quot;512&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;1506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 기본적으로 제공되는 메트릭 정보 외에도, 애플리케이션에서 필요한 메트릭을 직접 추가할 수 있을까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 기본적으로 제공되는 메트릭 정보 외에도, 애플리케이션에서 필요한 메트릭을 직접 추가할 수 있을까요?&lt;br /&gt;다행스럽게도 커스텀 메트릭을 추가하는 것도 가능합니다.&lt;/p&gt;
&lt;p data-end=&quot;429&quot; data-start=&quot;291&quot; data-ke-size=&quot;size16&quot;&gt;보통은 Micrometer의 MeterRegistry를 주입받아 메트릭을 추가합니다.&lt;br /&gt;Actuator가 메트릭 정보를 외부로 노출하는 창구라면, Micrometer는 메트릭을 생성하고 관리하는 도구에 가깝다고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;431&quot; data-ke-size=&quot;size16&quot;&gt;MeterRegistry는 Micrometer에서 모든 메트릭을 등록하고 관리하는 중앙 저장소 역할을 합니다.&lt;br /&gt;메트릭은 보통 다음과 같은 흐름으로 수집되고 노출됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1773234758917&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Application -&amp;gt; MeterRegistry -&amp;gt; Micrometer -&amp;gt; Actuator endpoint&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 애플리케이션에서 메트릭을 생성하고 MeterRegistry에 등록하면, Actuator가 이를 endpoint 형태로 외부에 노출하게 됩니다.&lt;br /&gt;Micrometer에서는 이러한 메트릭들을 Meter라는 개념으로 관리합니다. 대표적인 Meter의 종류로는 다음과 같은 것들이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;899&quot; data-start=&quot;778&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;808&quot; data-start=&quot;778&quot; data-section-id=&quot;rjgkkc&quot;&gt;Counter : 이벤트 발생 횟수 측정&lt;/li&gt;
&lt;li data-end=&quot;832&quot; data-start=&quot;809&quot; data-section-id=&quot;uw9dpp&quot;&gt;Gauge : 현재 값 측정&lt;/li&gt;
&lt;li data-end=&quot;860&quot; data-start=&quot;833&quot; data-section-id=&quot;1s313q4&quot;&gt;Timer : 작업 수행 시간 측정&lt;/li&gt;
&lt;li data-end=&quot;899&quot; data-start=&quot;861&quot; data-section-id=&quot;1g4khg2&quot;&gt;DistributionSummary : 값의 분포 측정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;940&quot; data-start=&quot;901&quot; data-ke-size=&quot;size16&quot;&gt;해당 내용은 Micrometer 공식 문서를 통해 확인할 수 있습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1773234446209&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;Supported Monitoring Systems :: Micrometer&quot; data-og-description=&quot;AppOptics, Atlas, Azure Monitor, Cloudwatch, Datadog, Datadog StatsD, Dynatrace, Elastic, Humio, Influx, KairosDB, New Relic, Prometheus, SignalFx, Sysdig StatsD, Telegraf StatsD, Wavefront&quot; data-og-host=&quot;docs.micrometer.io&quot; data-og-source-url=&quot;https://docs.micrometer.io/micrometer/reference/concepts/implementations.html&quot; data-og-url=&quot;https://docs.micrometer.io/micrometer/reference/concepts/implementations.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.micrometer.io/micrometer/reference/concepts/implementations.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.micrometer.io/micrometer/reference/concepts/implementations.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Supported Monitoring Systems :: Micrometer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AppOptics, Atlas, Azure Monitor, Cloudwatch, Datadog, Datadog StatsD, Dynatrace, Elastic, Humio, Influx, KairosDB, New Relic, Prometheus, SignalFx, Sysdig StatsD, Telegraf StatsD, Wavefront&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.micrometer.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 다음과 같이 작성하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QodTl/dJMcajuEbgU/fxxMtaMnSxv3mxZMoMaXVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QodTl/dJMcajuEbgU/fxxMtaMnSxv3mxZMoMaXVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QodTl/dJMcajuEbgU/fxxMtaMnSxv3mxZMoMaXVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQodTl%2FdJMcajuEbgU%2FfxxMtaMnSxv3mxZMoMaXVK%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;744&quot; height=&quot;186&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;count를 이용했고 다음과 같은것을 확인할 수 있습니다.&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;Redis 장애로 fallback 된 횟수&lt;/li&gt;
&lt;li&gt;unlock 실패 횟수&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;1192&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs6fGT/dJMcab4tgIj/ELJq6f5Fa5t0InFIJ3tpu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs6fGT/dJMcab4tgIj/ELJq6f5Fa5t0InFIJ3tpu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs6fGT/dJMcab4tgIj/ELJq6f5Fa5t0InFIJ3tpu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs6fGT%2FdJMcab4tgIj%2FELJq6f5Fa5t0InFIJ3tpu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;756&quot; height=&quot;147&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;acquire&amp;nbsp;time을&amp;nbsp;이용했고&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;것을&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;락&amp;nbsp;획득&amp;nbsp;성공까지&amp;nbsp;걸린&amp;nbsp;시간&lt;/li&gt;
&lt;li&gt;락&amp;nbsp;충돌을&amp;nbsp;판단하기까지&amp;nbsp;걸린&amp;nbsp;시간&lt;/li&gt;
&lt;li&gt;Redis&amp;nbsp;장애&amp;nbsp;발생&amp;nbsp;후&amp;nbsp;fallback&amp;nbsp;판단까지&amp;nbsp;걸린&amp;nbsp;시간&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;1172&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUJheY/dJMcac957RP/zSqpZsqWFdhK2lJ6plqxEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUJheY/dJMcac957RP/zSqpZsqWFdhK2lJ6plqxEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUJheY/dJMcac957RP/zSqpZsqWFdhK2lJ6plqxEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUJheY%2FdJMcac957RP%2FzSqpZsqWFdhK2lJ6plqxEk%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;834&quot; height=&quot;159&quot; data-origin-width=&quot;1172&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;task&amp;nbsp;time을&amp;nbsp;이용했고&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;것을&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;락&amp;nbsp;획득&amp;nbsp;이후&amp;nbsp;실제&amp;nbsp;비즈니스&amp;nbsp;로직&amp;nbsp;수행&amp;nbsp;시간&lt;/li&gt;
&lt;li&gt;락&amp;nbsp;구간&amp;nbsp;내부&amp;nbsp;작업의&amp;nbsp;병목&amp;nbsp;여부&lt;/li&gt;
&lt;li&gt;성공/실패&amp;nbsp;케이스별&amp;nbsp;작업&amp;nbsp;시간&amp;nbsp;차이&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-5-3&quot; data-message-id=&quot;cdf4f3c6-c77d-4e64-94a6-0dbf7e49cfb3&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메트릭을 메트릭을 생성하고, Actuator를 이용해서 생성한 메트릭을 노출을 시킬수 있습니다. 그걸 프로메테우스가 수집 하고 그라파나를 통해 화면으로 보여줄 수 있는거죠. 이전에 이러한 사실을 모르고 제가 설정을 잘못했는지에 대해 고민한적이 있었습니다. 하지만 알고보니 메트릭을 설정하지 않아서 발생한거라는 생각이 문득 드네요. 내일 프로메테우스랑 그라파나를 연동시켜봅시다.&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 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/549</guid>
      <comments>https://b-programmer.tistory.com/549#entry549comment</comments>
      <pubDate>Wed, 11 Mar 2026 23:41:42 +0900</pubDate>
    </item>
    <item>
      <title>Actuator</title>
      <link>https://b-programmer.tistory.com/548</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;k6를 통해 부하 테스트와 API 테스트를 진행하면서 한 가지 한계를 느꼈습니다.&lt;br /&gt;요청을 보내고 결과를 확인하는 것은 가능했지만, 실제로 부하가 걸리는 동안 애플리케이션 내부에서 어떤 변화가 일어나는지 화면으로 확인하기는 어렵다는 점입니다. 그래서 자연스럽게 이런 생각이 들었습니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;부하가 걸리는 상황을 시각적으로 모니터링할 수 있다면 더 좋지 않을까? &lt;/span&gt;이럴 때 떠올릴 수 있는 대표적인 도구가 바로 Grafana입니다. Grafana는 메트릭 데이터를 시각화하여 시스템 상태를 한눈에 확인할 수 있게 도와줍니다. 그런데 Grafana가 사용하려면 먼저 애플리케이션이 자신의 상태 정보를 외부로 제공할 수 있어야 합니다. Spring Boot에서는 이를 위해 Actuator를 사용할 수 있습니다. Actuator는 애플리케이션의 상태, 메트릭, 헬스 체크 정보 등을 외부에 노출해주는 기능이며, 이러한 정보들은 모니터링 도구와 연동하는 데 활용됩니다. 이번 기회에 Actuator가 무엇이고, 왜 사용하는지, 그리고 어떤 정보를 제공하는지 학습해보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Actuator란 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actulator는 Spring boot 애플리케이션의 운영 상태를 외부에서 확인하고 관리할 수 있도록 정보를 제공하는 기능입니다. 애플리케이션이 실행되는 동안 내부에서 발생하는 다양한 정보들을 HTTP endpoint 형태로 노출하여, 개발자나 운영 환경에서 이를 확인 할 수 있도록 도와줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 먼저 그레들에 Actulator를 등록합니다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-actuator'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면, 어떠한 정보들을 HTTP endPoint형태로 제공할까요?&lt;/h3&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;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6VAOp/dJMcach1eOZ/JCg5pNJk2Q3siv0HvQpkg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6VAOp/dJMcach1eOZ/JCg5pNJk2Q3siv0HvQpkg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6VAOp/dJMcach1eOZ/JCg5pNJk2Q3siv0HvQpkg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6VAOp%2FdJMcach1eOZ%2FJCg5pNJk2Q3siv0HvQpkg1%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;514&quot; height=&quot;270&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인을 해보니 Rest형식으로 제공하고 있는것을 확인 할 수 있었습니다. 눈에 띄이는것은 health부분입니다.&lt;br /&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;374&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh1kO5/dJMcacPQQvl/oGCqySHM9lkRFFW89GDJ41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh1kO5/dJMcacPQQvl/oGCqySHM9lkRFFW89GDJ41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh1kO5/dJMcacPQQvl/oGCqySHM9lkRFFW89GDJ41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh1kO5%2FdJMcacPQQvl%2FoGCqySHM9lkRFFW89GDJ41%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;242&quot; height=&quot;175&quot; data-origin-width=&quot;374&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 상태가 UP으로 되어 있네요. 참고로 link에 group을 path로 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 Actuator가 기본값만 노출하고 있는 상태입니다. 그래서 /actuator에 들어가면 health 관련 링크만 보이는 것입니다.&lt;br /&gt;다른 endpoint는 열어두지않는 상태입니다. 찾아보니 보안상 문제로 많은 endpoint들을 오픈하지 않는다고 합니다. &lt;br /&gt;그렇다면, 다른 endpoint들도 확인을 하려면 어떻게 해야 할까요?&lt;/p&gt;
&lt;figure id=&quot;og_1773210477169&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;Endpoints :: Spring Boot&quot; data-og-description=&quot;If you add a @Bean annotated with @Endpoint, any methods annotated with @ReadOperation, @WriteOperation, or @DeleteOperation are automatically exposed over JMX and, in a web application, over HTTP as well. Endpoints can be exposed over HTTP by using Jersey&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-boot/reference/actuator/endpoints.html&quot; data-og-url=&quot;https://docs.spring.io/spring-boot/reference/actuator/endpoints.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/reference/actuator/endpoints.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-boot/reference/actuator/endpoints.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Endpoints :: Spring Boot&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;If you add a @Bean annotated with @Endpoint, any methods annotated with @ReadOperation, @WriteOperation, or @DeleteOperation are automatically exposed over JMX and, in a web application, over HTTP as well. Endpoints can be exposed over HTTP by using Jersey&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문서를 보면 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1773210680700&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요렇게 지정하면, health, info, metrics,prometheus가 노출됩니다.&lt;br /&gt;만약 include를 *로 지정한다면 모든 endpoint를 열 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;1388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1x7Mu/dJMcabcjCZ1/dgSZWJPRcAuVEkeK9LhWiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1x7Mu/dJMcabcjCZ1/dgSZWJPRcAuVEkeK9LhWiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1x7Mu/dJMcabcjCZ1/dgSZWJPRcAuVEkeK9LhWiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1x7Mu%2FdJMcabcjCZ1%2FdgSZWJPRcAuVEkeK9LhWiK%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;506&quot; height=&quot;438&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;1388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actuator를 통해 다양한 정보를 endpoint 형태로 제공할 수 있다는 것을 확인할 수 있습니다.&lt;br /&gt;또한 일반적인 endpoint 외에도 특수한 목적을 가진 endpoint들이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 다음과 같은 endpoint가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1820&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y4iiP/dJMcaaEwggU/6TMAOpW3Awe3krTWWxHKAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y4iiP/dJMcaaEwggU/6TMAOpW3Awe3krTWWxHKAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y4iiP/dJMcaaEwggU/6TMAOpW3Awe3krTWWxHKAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy4iiP%2FdJMcaaEwggU%2F6TMAOpW3Awe3krTWWxHKAk%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;1820&quot; height=&quot;592&quot; data-origin-width=&quot;1820&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;553&quot; data-start=&quot;412&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;451&quot; data-start=&quot;412&quot; data-section-id=&quot;1iyxpbp&quot;&gt;heapdump : JVM Heap Dump 파일을 제공합니다.&lt;/li&gt;
&lt;li data-end=&quot;495&quot; data-start=&quot;452&quot; data-section-id=&quot;1t642gz&quot;&gt;logfile : 애플리케이션 로그 파일의 내용을 확인할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;553&quot; data-start=&quot;496&quot; data-section-id=&quot;fj8bj9&quot;&gt;prometheus : Prometheus 서버가 수집할 수 있는 형식으로 메트릭을 노출합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;631&quot; data-start=&quot;555&quot; data-ke-size=&quot;size16&quot;&gt;각 endpoint는 동작에 필요한 조건이 충족되어야 하며, 조건이 맞지 않는 경우에는 endpoint 자체가 생성되지 않을 수도 있습니다.&lt;br /&gt;참고로 prometheus는 의존성을 추가해야 합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'io.micrometer:micrometer-registry-prometheus'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이제 실제로 테스트를 진행해봅시다.&lt;/h2&gt;
&lt;pre id=&quot;code_1773213592179&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이렇게 4개를 등록해보겠습니다. 현재 상태에서는 prometheus의 의존성이 추가가 되지 않았기 때문에 앞에 3개만 endpoint로 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc5H0N/dJMcagxVvOo/7jfRIeZ33bAaZMOBemmjHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc5H0N/dJMcagxVvOo/7jfRIeZ33bAaZMOBemmjHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc5H0N/dJMcagxVvOo/7jfRIeZ33bAaZMOBemmjHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc5H0N%2FdJMcagxVvOo%2F7jfRIeZ33bAaZMOBemmjHK%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;586&quot; height=&quot;433&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 Actuator를 이용해서 무엇을 할 수 있는데?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actuator를 사용하면 애플리케이션의 현재 상태를 확인할 수 있으며, 추후에는 메트릭 정보를 수집하고 모니터링하는 용도로 활용할 수 있습니다. 예를 들어 health endpoint를 통해 애플리케이션이 정상적으로 동작하고 있는지 확인할 수 있고, metrics나 prometheus endpoint를 통해 JVM 메모리 사용량, HTTP 요청 수와 같은 다양한 메트릭 정보를 확인할 수 있습니다. 이러한 정보들은 Prometheus와 같은 모니터링 시스템이 수집하여 Grafana와 같은 도구를 통해 시각적으로 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 Actuator가 어떤 것인지 알아보았습니다. Actuator를 사용하면 현재 애플리케이션의 상태를 확인할 수 있으며, 추후에는 다양한 메트릭 정보를 확인하는 데 활용할 수 있다는 점을 알 수 있었습니다. 또한 운영 환경에서는 보안 문제로 인해 모든 endpoint를 외부에 노출하지 않고, Actuator 포트를 분리하거나 내부 네트워크에서만 접근할 수 있도록 구성하여 관리한다고 합니다. 다만 이 부분은 아직 직접 구성해보지는 않아 자세하게는 알지 못했습니다. 추후에는 Actuator를 통해 수집한 메트릭 정보를 Prometheus와 Grafana와 연동하여 모니터링 환경을 구성해보는 것도 좋을 것 같습니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/548</guid>
      <comments>https://b-programmer.tistory.com/548#entry548comment</comments>
      <pubDate>Wed, 11 Mar 2026 16:36:02 +0900</pubDate>
    </item>
    <item>
      <title>OpenAI는 어떻게 PostgreSQL을 스케일했을까</title>
      <link>https://b-programmer.tistory.com/547</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;국내에서는 많은 기업들이 &lt;span&gt;&lt;span&gt;MySQL&lt;/span&gt;&lt;/span&gt;을 사용하는 경우가 많습니다. 그래서 다른 회사들이 어떤 데이터베이스를 사용하는지에 대해서는 크게 의식하지 않았습니다. 그러다가 우연히 &lt;span&gt;&lt;span&gt;OpenAI&lt;/span&gt;&lt;/span&gt;가 수억 명의 사용자 규모까지 서비스를 확장한 과정을 설명한 글을 읽게 되었습니다.&lt;br /&gt;(&lt;a href=&quot;https://blog.bytebytego.com/p/how-openai-scaled-to-800-million&quot;&gt;https://blog.bytebytego.com/p/how-openai-scaled-to-800-million&lt;/a&gt;) 글을 읽다 보니 OpenAI가 &lt;span&gt;&lt;span&gt;PostgreSQL&lt;/span&gt;&lt;/span&gt;을 사용하고 있다는 점이 눈에 들어왔습니다. 다양한 데이터베이스가 존재하는데 왜 PostgreSQL을 선택했는지 궁금해졌습니다. 물론 이 글에서 PostgreSQL을 선택한 명확한 이유가 직접적으로 설명되지는 않습니다. 다만 일반적으로 PostgreSQL은 안정성과 성숙한 생태계를 갖춘 관계형 데이터베이스로 알려져 있습니다. 또한 복잡한 쿼리 처리 능력과 확장 기능이 강점으로 꼽히기 때문에 많은 서비스에서 기본적인 선택지로 사용되기도 합니다. 결국 OpenAI가 PostgreSQL을 선택한 이유가 반드시 특별한 기술적 이유 때문이라고 보기는 어렵지만, 안정적이고 검증된 관계형 데이터베이스라는 점에서 자연스러운 선택이었을 가능성이 높다고 생각했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Open Ai는 왜 스케일을 선택하였을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답은 간단합니다. 사용자가 급격히 증가했기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;293&quot; data-start=&quot;170&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;OpenAI&lt;/span&gt;&lt;/span&gt;의 서비스는 매우 빠르게 성장했습니다. 예를 들어 GPT 출시 이후 API 서비스와 기업 고객이 늘어나면서 사용자 규모가 수억 명 수준까지 확대되었습니다.&lt;/p&gt;
&lt;p data-end=&quot;393&quot; data-start=&quot;295&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 늘어나면 자연스럽게 서비스 요청도 증가합니다.&lt;br /&gt;사용자가 증가하면 API 요청이 많아지고, 그 결과 데이터베이스에 대한 조회와 쓰기 요청 역시 함께 증가하게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;438&quot; data-start=&quot;395&quot; data-ke-size=&quot;size16&quot;&gt;만약 이런 상황에서 데이터베이스를 스케일하지 않는다면 어떤 문제가 발생할까요?&lt;/p&gt;
&lt;p data-end=&quot;583&quot; data-start=&quot;440&quot; data-ke-size=&quot;size16&quot;&gt;데이터베이스가 증가한 요청을 감당하지 못하게 되면 응답 속도가 느려지고, 동시에 데이터베이스 연결(connection)이 급격히 늘어나게 됩니다. 결국 이러한 상황이 지속되면 데이터베이스가 병목 지점이 되어 서비스 장애로 이어질 가능성이 높아집니다.&lt;/p&gt;
&lt;p data-end=&quot;645&quot; data-start=&quot;585&quot; data-ke-size=&quot;size16&quot;&gt;따라서 이러한 문제를 방지하기 위해 데이터베이스를 확장(Scaling) 하는 전략이 필요하게 됩니다.&lt;/p&gt;
&lt;h2 data-end=&quot;675&quot; data-start=&quot;658&quot; data-ke-size=&quot;size26&quot;&gt;DB확장 포인트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스를 확장하기 위해서는 먼저 어디에서 병목이 발생하는지를 생각해 볼 필요가 있습니다.&lt;br /&gt;즉, 쓰기 성능을 확장해야 하는지, 아니면 읽기 성능을 확장해야 하는지를 판단해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;358&quot; data-start=&quot;219&quot; data-ke-size=&quot;size16&quot;&gt;일반적으로는 읽기(Read) 성능부터 확장하는 경우가 많습니다.&lt;br /&gt;먼저 성능 개선 단계로 쿼리 최적화 &amp;rarr; 인덱스 사용 &amp;rarr; 캐시 도입을 진행합니다. 그래도 문제가 해결되지 않는다면 레플리케이션(Replication)을 통해 읽기 트래픽을 분산합니다.&lt;/p&gt;
&lt;p data-end=&quot;409&quot; data-start=&quot;360&quot; data-ke-size=&quot;size16&quot;&gt;이후에도 트래픽이 계속 증가한다면 쓰기(Write) 성능 확장을 고려하게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;411&quot; data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 것처럼 &lt;span&gt;&lt;span&gt;OpenAI&lt;/span&gt;&lt;/span&gt;의 사용자 수는 급격하게 증가했습니다. 사용자 수가 증가했다는 것은 그만큼 쓰기 요청 또한 함께 증가했을 가능성이 높습니다.&lt;/p&gt;
&lt;p data-end=&quot;634&quot; data-start=&quot;534&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 자연스럽게 쓰기 성능 확장도 필요해 보입니다.&lt;br /&gt;하지만 OpenAI는 쓰기 확장을 선택하지 않고, 레플리케이션을 통해 읽기 트래픽을 분산하는 구조를 사용했습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;678&quot; data-start=&quot;636&quot; data-ke-size=&quot;size26&quot;&gt;그렇다면 어떻게 이런 구조로도 대규모 트래픽을 처리할 수 있었을까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 생각보다 쓰기 요청이 많지 않기 때문입니다.&lt;br /&gt;크게 3가지 이유가 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대부분의 서비스는 읽기 &amp;gt;쓰기 구조입니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 사용자 프로필 조회, 설정 조회, 대화 기록 조회, API 상태 조회등 같은 요청들은 대부분 읽기 요청입니다.&lt;br /&gt;그래서 보통 읽기 90% 쓰기 10%정도 구조가 됩니다. 그래서 Open AI도 읽기 트래픽을 분산 하는 것만으로도 큰 효과를 얻을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 OpenAI는 ChatGPT라는 LLM을 사용합니다. LLM은 채팅을 입력을 하게 되면 그것에대한 응답을 작성해줍니다.&lt;br /&gt;여기서 OpenAI가 쓰기작업을 한다고 여겨집니다. 그렇다면, 읽기 90% 쓰기 10%는 OpenAI한테는 다른 의미인걸까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그 이유는 ChatGPT 응답은 DB가 아니라 GPU에서 처리가 되어지기 때문입니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 질문을 하면 실제로 일어나는 일은 이렇습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1773149148551&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;User 질문 -&amp;gt; API Server -&amp;gt; LLM 모델 실행(GPU) -&amp;gt; 응답 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심 연산은 모델 추론입니다. 이 과정은 DB, SQL과 거의 관련이 없습니다.&lt;br /&gt;즉, ChatGPT 응답 생성 은 GPU 계산입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 OpenAI도 &lt;span&gt;읽기 트래픽을 분산&lt;/span&gt;하는 것만으로도 큰 효과를 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 이해한 내용으로는 LLM 요청 자체는 데이터베이스의 쓰기 작업과 직접적인 관련이 없다는 점을 알 수 있었습니다. 하지만 여전히 의문이 완전히 해소된 것은 아닙니다.&lt;/p&gt;
&lt;p data-end=&quot;343&quot; data-start=&quot;241&quot; data-ke-size=&quot;size16&quot;&gt;LLM의 응답 생성이 GPU에서 이루어진다고 하더라도, 수억 명 규모의 사용자에게 발생하는 서비스 트래픽을 생각하면 여전히 상당한 쓰기 요청이 발생할 가능성이 있기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;392&quot; data-start=&quot;345&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자 수가 증가하면 자연스럽게 다음과 같은 데이터가 기록될 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;451&quot; data-start=&quot;394&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;408&quot; data-start=&quot;394&quot; data-section-id=&quot;vvkgxd&quot;&gt;API 사용량 기록&lt;/li&gt;
&lt;li data-end=&quot;423&quot; data-start=&quot;409&quot; data-section-id=&quot;lryswy&quot;&gt;billing 정보&lt;/li&gt;
&lt;li data-end=&quot;437&quot; data-start=&quot;424&quot; data-section-id=&quot;3f9bbw&quot;&gt;사용자 활동 로그&lt;/li&gt;
&lt;li data-end=&quot;451&quot; data-start=&quot;438&quot; data-section-id=&quot;1hoye4s&quot;&gt;시스템 메타데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;540&quot; data-start=&quot;453&quot; data-ke-size=&quot;size16&quot;&gt;이러한 정보들은 결국 데이터베이스에 저장되어야 합니다. 그렇다면 8억 명 규모의 사용자 환경에서 발생하는 쓰기 트래픽을 어떻게 감당할 수 있었을까요?&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-section-id=&quot;1fshqm6&quot; data-start=&quot;389&quot; data-end=&quot;415&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;OpenAI는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Primary DB가 하는 일을 최소화했습니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Primary 역할은 쓰기만 담당하고 읽기는 Replica로 보냅니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1262&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7sLOB/dJMcagkn9JP/3altVWDcrxnVwp2JiFPVLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7sLOB/dJMcagkn9JP/3altVWDcrxnVwp2JiFPVLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7sLOB/dJMcagkn9JP/3altVWDcrxnVwp2JiFPVLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7sLOB%2FdJMcagkn9JP%2F3altVWDcrxnVwp2JiFPVLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;538&quot; height=&quot;230&quot; data-origin-width=&quot;1262&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;522&quot; data-start=&quot;515&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 되면 Primary DB는 쓰기 작업만 처리하게 됩니다.&lt;br /&gt;하지만 읽기와 쓰기 데이터베이스를 분리했다고 하더라도 데이터베이스에 부담이 완전히 사라지는 것은 아닙니다.&lt;/p&gt;
&lt;p data-end=&quot;712&quot; data-start=&quot;711&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 &lt;span&gt;&lt;span&gt;OpenAI&lt;/span&gt;&lt;/span&gt;는 데이터베이스에 부담을 주는 요소를 최대한 줄이는 방향으로 시스템을 설계했습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 느린 쿼리를 제거하고, 인덱스를 최적화하며, connection pool을 사용하는 방식으로 데이터베이스 부하를 줄였습니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;특히 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;PostgreSQL&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;은 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;connection = process&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 구조를 가지고 있습니다.&lt;br /&gt;&lt;/span&gt;즉, 데이터베이스 연결이 하나 생성될 때마다 하나의 프로세스가 생성됩니다.&lt;/p&gt;
&lt;p data-end=&quot;530&quot; data-start=&quot;444&quot; data-ke-size=&quot;size16&quot;&gt;따라서 connection 수가 과도하게 증가하면 메모리 사용량이 증가하고 context switching이 발생하면서 성능이 저하될 수 있습니다.&lt;br /&gt;이를 해결하기 위해 connection pool을 사용하여 데이터베이스에 직접 연결되는 connection 수를 제한하고, 효율적으로 관리할 수 있도록 했습니다.&lt;/p&gt;
&lt;p data-end=&quot;712&quot; data-start=&quot;711&quot; data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 connection 하나마다 하나의 process가 생성되는 구조를 가지고 있습니다.&lt;br /&gt;이 방식은 thread 기반 구조에 비해 약간의 성능 비용이 발생할 수 있지만, 대신 높은 안정성을 제공하는 장점이 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;434&quot; data-start=&quot;288&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 하나의 process에서 문제가 발생하더라도 해당 connection만 종료되며, 다른 connection에는 영향을 주지 않습니다.&lt;br /&gt;즉, 하나의 요청에서 오류가 발생하더라도 데이터베이스 전체가 영향을 받지 않도록 격리된 구조로 동작합니다.&lt;/p&gt;
&lt;p data-end=&quot;509&quot; data-start=&quot;436&quot; data-ke-size=&quot;size16&quot;&gt;이러한 특성 때문에 PostgreSQL은 안정성과 신뢰성이 중요한 환경에서 널리 사용되는 데이터베이스 중 하나로 평가됩니다.&lt;/p&gt;
&lt;h2 data-end=&quot;509&quot; data-start=&quot;436&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이것이 OpenAI의 PostgreSQL 확장 전략의 전부는 아닙니다.&lt;br /&gt;글에서는 MVCC를 비롯해 더 깊이 있는 내용들도 함께 다루고 있으며, 실제로 어떤 방식으로 문제를 해결했는지에 대한 설명도 이어집니다.&lt;/p&gt;
&lt;p data-end=&quot;406&quot; data-start=&quot;288&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 그중에서도 OpenAI가 PostgreSQL을 어떤 방향으로 확장했는지, 그리고 왜 쓰기 확장보다 읽기 확장과 Primary DB 부하 최소화에 집중했는지를 중심으로 정리해보았습니다.&lt;/p&gt;
&lt;p data-end=&quot;503&quot; data-start=&quot;408&quot; data-ke-size=&quot;size16&quot;&gt;결국 중요한 점은 데이터베이스를 무작정 바꾸는 것이 아니라, 현재 사용 중인 데이터베이스를 어떻게 효율적으로 운영하고 확장할 것인지 고민하는 과정이라고 생각합니다.&lt;/p&gt;
&lt;p data-end=&quot;503&quot; data-start=&quot;408&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://blog.bytebytego.com/p/how-openai-scaled-to-800-million&quot;&gt;https://blog.bytebytego.com/p/how-openai-scaled-to-800-million&lt;/a&gt; &amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://openai.com/ko-KR/index/scaling-postgresql/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://openai.com/ko-KR/index/scaling-postgresql/&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/547</guid>
      <comments>https://b-programmer.tistory.com/547#entry547comment</comments>
      <pubDate>Tue, 10 Mar 2026 23:21:51 +0900</pubDate>
    </item>
    <item>
      <title>프록시 JDBC(2)</title>
      <link>https://b-programmer.tistory.com/546</link>
      <description>&lt;figure id=&quot;og_1773032232310&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;프록시 JDBC(1)&quot; data-og-description=&quot;이전에 저는 파일을 통해 slow쿼리를 계산을 진행하였습니다. 하지만 그러다 보니 실시간으로 메시지를 보여주지 못하였습니다. 처음 의도가 네트워크 상황이라던지 여러 요인들이 존재하기 때&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/545&quot; data-og-url=&quot;https://b-programmer.tistory.com/545&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bRLV7h/dJMb9kmaS4L/2rSNqJ4QGmkHODvQvZ4E20/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/BcCWq/dJMb82eK829/gcqVh63dHPJWHLukIFC9b0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/brRoyU/dJMb9gxjva0/SegqQLfv8Z62m8nMVjhk6K/img.jpg?width=383&amp;amp;height=248&amp;amp;face=108_72_174_144&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/545&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/545&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bRLV7h/dJMb9kmaS4L/2rSNqJ4QGmkHODvQvZ4E20/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/BcCWq/dJMb82eK829/gcqVh63dHPJWHLukIFC9b0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/brRoyU/dJMb9gxjva0/SegqQLfv8Z62m8nMVjhk6K/img.jpg?width=383&amp;amp;height=248&amp;amp;face=108_72_174_144');&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;프록시 JDBC(1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이전에 저는 파일을 통해 slow쿼리를 계산을 진행하였습니다. 하지만 그러다 보니 실시간으로 메시지를 보여주지 못하였습니다. 처음 의도가 네트워크 상황이라던지 여러 요인들이 존재하기 때&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분량과 시간 문제로 다음 편에 작성하게 되었습니다.&lt;br /&gt;이전 장에서는 DelegatingDataSource와 Wrapper를 사용해야 하는 이유를 살펴보았습니다. 우리가 최종적으로 관찰하고 싶은 지점은 execute() 메서드입니다. 이 메서드는 실제로 SQL을 실행하는 역할을 합니다. 하지만 SQL은 execute() 시점에 갑자기 만들어지는 것이 아닙니다. SQL을 실행하려면 그보다 앞서 SQL을 준비하는 과정이 필요합니다. 그리고 그 역할을 담당하는 객체가 바로 PreparedStatement입니다. 즉 우리가 관심 있는 지점은 execute()이지만, execute()를 이해하려면 결국 PreparedStatement까지 올라가야 합니다. 그런데 PreparedStatement는 스스로 생성되지 않고, Connection을 통해 생성됩니다. 또한 Connection 역시 직접 생성되는 객체가 아니라 DataSource를 통해 획득됩니다. 정리하면 JDBC 실행 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1773033489692&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DataSource &amp;rarr; Connection &amp;rarr; PreparedStatement &amp;rarr; execute()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 때문에 execute() 호출을 자동으로 관찰하려면, 가장 안쪽 메서드만 따로 다룰 수 있는 것이 아니라 객체가 생성되는 흐름의 시작점부터 개입해야 합니다. 그래서 DataSource부터 Wrapping이 필요했던 것입니다. 다행스럽게도 Spring은 DataSource를 감쌀 수 있는 DelegatingDataSource를 제공합니다. 이를 통해 DataSource 단계에는 비교적 쉽게 개입할 수 있습니다. 다만DelegatingDataSource가 Connection부터 PreparedStatement, execute()까지 자동으로 모두 감싸주는 것은 아닙니다. 즉 DataSource 이후의 흐름에 대해서는 개발자가 직접 Wrapping을 구현해야 합니다. 이번 글에서는 이러한 JDBC 실행 흐름을 따라가며,&lt;br /&gt;Connection과 PreparedStatement를 어떻게 감싸고, 최종적으로 execute() 시점을 어떻게 관찰할 수 있는지 알아보겠습니다.&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;br /&gt;대표적으로 프록시 패턴을 직접 구현하는 방법, JDK 동적 프록시를 사용하는 방법, 그리고 CGLIB과 같은 라이브러리를 사용하는 방법 등이 있습니다.&lt;br /&gt;이번 글에서는 이 중 JDK 동적 프록시를 이용해 구현해보겠습니다.&lt;/p&gt;
&lt;p data-end=&quot;283&quot; data-start=&quot;243&quot; data-ke-size=&quot;size16&quot;&gt;그 전에, 왜 JDK 동적 프록시 방식을 선택했는지 먼저 살펴보겠습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 103px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;방법&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;JDK&amp;nbsp;동적&amp;nbsp;프록시&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;외부 의존성 없음, execute* 시점에서 정확한 SQL 실행 시간 계측 가능, traceId 같은 애플리케이션 정보 연동 쉬움&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;프록시 코드 직접 구현 및 유지보수 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;Hibernate&amp;nbsp;StatementInspector&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;JPA 환경에서는 자연스럽게 SQL을 가로챌 수 있음&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot;&gt;JDBC 직접 호출이나 다른 ORM 경로의 SQL은 잡지 못할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 16px;&quot;&gt;Spring&amp;nbsp;AOP&amp;nbsp;(Service/Repo&amp;nbsp;계층)&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 16px;&quot;&gt;적용이 쉽고 코드 스타일 일관성 유지 가능&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 16px;&quot;&gt;실제 DB execute 시간이 아니라 서비스 로직 시간까지 포함될 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법은 크게 세 가지가 있습니다. 하지만 제가 필요한 것은 SQL 실행 시간을 측정하여 해당 쿼리가 슬로우 쿼리인지 판단하는 것입니다.&lt;br /&gt;먼저 Hibernate StatementInspector의 경우 JPA 환경에서 실행되는 SQL을 가로챌 수 있다는 장점이 있습니다. 하지만 JDBC를 직접 사용하는 경우나 다른 경로로 실행되는 SQL은 잡지 못할 수 있다는 한계가 있습니다. 또한 Spring AOP를 사용하는 방식은 서비스나 리포지토리 계층에서 메서드 실행 시간을 측정하게 됩니다. 이 경우 실제 SQL 실행 시간뿐만 아니라 서비스 로직 수행 시간까지 함께 포함될 수 있습니다. 따라서 제가 원하는 방식은 서비스 로직의 시간이 포함되지 않고, SQL이 실제로 실행되는 시점의 시간만 측정할 수 있는 방법이어야 합니다.&lt;br /&gt;이를 위해서는 JDBC에서 SQL이 실제로 실행되는 지점인 execute() 호출 시점을 기준으로 시간을 측정하는 방식이 가장 적절하다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 DataSource를 Wrapper 클래스로 만들어야 합니다.&lt;br /&gt;이렇게 접근한 이유는 execute() 호출 시점을 관찰하려면, 객체가 생성되는 흐름의 시작점인 DataSource부터 개입해야 한다고 판단했기 때문입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;854&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sV2bP/dJMcagYYN47/gG08w10fbp1bfvNzl9wNfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sV2bP/dJMcagYYN47/gG08w10fbp1bfvNzl9wNfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sV2bP/dJMcagYYN47/gG08w10fbp1bfvNzl9wNfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsV2bP%2FdJMcagYYN47%2FgG08w10fbp1bfvNzl9wNfk%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;584&quot; height=&quot;334&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;854&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 DataSource를 직접 Wrapping하려고 살펴보니 한 가지 문제가 있었습니다.&lt;br /&gt;DataSource는 단순히 getConnection() 하나만 가진 객체가 아니라, 여러 메서드를 함께 가지고 있는 인터페이스였습니다.&lt;br /&gt;즉, DataSource를 직접 구현하는 방식으로 Wrapper를 만들 경우, 실제로 필요한 기능은 많지 않더라도 인터페이스에 선언된 수많은 메서드를 함께 구현해야 하는 번거로움이 발생했습니다. 결국 DataSource를 Wrapping한다는 것은 단순히 getConnection() 하나만 다루는 일이 아니라, 기존 DataSource가 가지고 있는 동작 전체를 유지하면서 필요한 지점만 확장해야 하는 작업이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행스럽게도 Spring에서는 DataSource를 wrappring해놓은 클래스를 제공하고 있었습니다.&lt;br /&gt;그렇다면 DataSource를 제외한 나머지를 어떻게 하면 wrapping 할 수 있을지 살펴보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;wrapping 방법&lt;/h3&gt;
&lt;pre id=&quot;code_1773044173281&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Proxy.newProxyInstance()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 프록시객체로 만드는 방법입니다. 어떤 정보가 필요한지 체크 해봅시다.&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;newProxyInstance(ClassLoader loader,
                 Class&amp;lt;?&amp;gt;[] interfaces,
                 InvocationHandler h)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 시그니처를 살펴보면 세 가지 파라미터가 필요합니다.&lt;br /&gt;코드를 확인해보니 자바의 리플렉션(Reflection) 기술을 이용해 런타임에 프록시 객체를 생성하는 방식이라는 것을 알 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ClassLoader&lt;/h4&gt;
&lt;p data-end=&quot;591&quot; data-start=&quot;519&quot; data-ke-size=&quot;size16&quot;&gt;프록시 객체를 생성할 때 사용할 클래스 로더입니다.&lt;br /&gt;보통 프록시를 생성할 인터페이스의 클래스 로더를 그대로 사용합니다.&lt;br /&gt;&lt;br /&gt;그렇다면 클래스 로더를 가져오는 이유가 무엇일까요?&amp;nbsp;&lt;br /&gt;자바에서는 모든 클래스가 ClassLoader를 통해 JVM에 로딩됩니다.&lt;br /&gt;JDK 동적 프록시는 런타임에 새로운 프록시 클래스를 생성하기 때문에, 이 클래스 역시 JVM에 로딩되어야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;409&quot; data-start=&quot;282&quot; data-ke-size=&quot;size16&quot;&gt;따라서 JVM은 새로 생성된 프록시 클래스를 어떤 ClassLoader를 통해 로딩할지 알아야 합니다.&lt;br /&gt;그래서&lt;span style=&quot;background-color: #dddddd;&quot;&gt; Proxy.newProxyInstance()&lt;/span&gt; 메서드는 ClassLoader를 파라미터로 요구합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1773045637765&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Connection.class.getClassLoader(),&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-end=&quot;591&quot; data-start=&quot;519&quot; data-ke-size=&quot;size20&quot;&gt;Interfaces&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 객체가 구현해야 할 인터페이스 목록입니다.&lt;br /&gt;JDK 동적 프록시는 인터페이스 기반으로 동작하기 때문에, 반드시 구현할 인터페이스를 전달해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClassLoader는 프록시 클래스를 JVM에 로딩하기 위한 정보이고, interfaces는 프록시 객체가 구현해야 할 인터페이스 목록입니다. 따라서 전달된 인터페이스는 해당 ClassLoader에서 접근 가능한 타입이어야 하며, 보통은 인터페이스를 로딩한 ClassLoader를 그대로 사용하는 방식이 가장 안전합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1773045647644&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;new Class[]{Connection.class},&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;InvocationHandler&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;프록시 객체의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;메서드 호출을 실제로 처리하는 객체입니다.&lt;br /&gt;프록시 객체의 메서드가 호출되면, 실제 메서드가 바로 실행되는 것이 아니라 이 InvocationHandler의 invoke() 메서드가 먼저 실행됩니다.&lt;/p&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;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ4Kar/dJMcabKbdLo/U7mjemxocxpNVu9EJ7bnlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ4Kar/dJMcabKbdLo/U7mjemxocxpNVu9EJ7bnlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ4Kar/dJMcabKbdLo/U7mjemxocxpNVu9EJ7bnlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ4Kar%2FdJMcabKbdLo%2FU7mjemxocxpNVu9EJ7bnlK%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;737&quot; height=&quot;84&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 여기에서 진행이 되어지는 것이 무엇일까요?&lt;br /&gt;InvocationHandler는 프록시 객체에서 발생하는 모든 메서드 호출을 가로채고, 그 호출에 대해 추가적인 로직을 수행할 수 있는 핵심 지점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 몇개 들어봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1773046449004&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Object result = invoke(conn, method, args);
if (result instanceof PreparedStatement ps) {
  return wrapPreparedStatement(ps, (String) args[0]);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 메소드에 아무것도 실행하지 않고 원 상태를 실행을 시켜줬습니다.&amp;nbsp;&lt;br /&gt;만약, result가 PrepredStatement인 경우 PrepraredStatmet를 wrapping한 결과로 넘어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 또 의문이 들었습니다. 이럴거면 원문에서 PrepredStatement가 리턴되어지는것으로 바꾸면 되지 않을까요?&lt;br /&gt;아쉽게도 불가능합니다. 그 이유는 Connection -&amp;gt; PrepredStatement를 리턴해주는 메소드들을 살펴봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1773047011728&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PreparedStatement prepareStatement(String sql)
        throws SQLException;
        
PreparedStatement prepareStatement(String sql, int resultSetType,
                                       int resultSetConcurrency)
        throws SQLException;
        
PeparedStatement prepareStatement(String sql, int resultSetType,
                                       int resultSetConcurrency, int resultSetHoldability)
        throws SQLException;
        
PreparedStatement prepareStatement(String sql, int autoGeneratedKeys)
        throws SQLException;

PreparedStatement prepareStatement(String sql, int columnIndexes[])
        throws SQLException;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나같이 전부 sql문이 필요합니다. 하지만 api를 실행하기 전까지는 어떤 sql이 실행이 되어졌는지는 모릅니다.&lt;br /&gt;그렇다면 방법은 하나뿐입니다. 이것또한 프록시로 만드는 방법입니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;다시 코드를 보면&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773047110904&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Object result = invoke(conn, method, args);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;invoke가 되어지는 부분이 존재합니다. 보시면 conn, method, args정보가 들어가있습니다.&lt;br /&gt;우리가 주목해야 하는 부분은 바로 args입니다. 그러니까 이렇게 처리하게 되면 2가지 장점이 있습니다.&lt;br /&gt;첫 번째는 객체 도중에 추가 기능을 추가하는 것이 있고, 두 번째는 매개변수를 개발자가 몰라도 넣을 수 있다는 점입니다.&lt;br /&gt;&lt;br /&gt;이를 이용하게 하면&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773047584764&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (result instanceof PreparedStatement ps) {
  return wrapPreparedStatement(ps, (String) args[0]);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요렇게 사용할 수 있습니다. 만약 결과가 PreparedStatemet라면 result값이랑 매개변수 첫번째를 넘겨줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 두번째 예시입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1773047758839&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return (PreparedStatement) Proxy.newProxyInstance(
    PreparedStatement.class.getClassLoader(),
    new Class[]{PreparedStatement.class},
    (proxy, method, args) -&amp;gt; {
      long startedNanos = System.nanoTime();
      try {
        return invoke(target, method, args);
      } finally {
        monitor.collect(sql, toElapsedMillis(startedNanos));
      }
    }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 살짝 다른데요.&lt;br /&gt;우리가 주목을 해야 하는 부분은 바로&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773047875100&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;long startedNanos = System.nanoTime();
try {
  return invoke(target, method, args);
} finally {
  monitor.collect(sql, toElapsedMillis(startedNanos));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 바로 wrapper로만 사용하지 않고 프록시까지 사용하는 방법입니다. invoke위를 보시면 startedNanos가 존재하는 것을 확인 할 수 있습니다. 그리고 나서 invoke가 실행이 된다면? 물론 약간의 네트워크 시간이 있을 수 는 있겠지만, 그것이 확인하는데 문제가 될정도는 아닐겁니다. 만약, 문제가 된다면 직접 db 로그를 확인 하는 방법이 더 효율적이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 나서 객체가 생성이 완료가 된 이후에 시간 측정을 완료한다면, slow쿼리를 측정할 수 있습니다.&lt;br /&gt;이는 프록시 객체로 만들어서 추가 기능을 만드는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 예시를 살펴봤는데요. 어째서 Connection도 wrapping을 하는지 이해를 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어제에 이어서 코드 분석을 진행하였습니다. 어째서 jdk 동적 프록시를 사용하는지 알아보았습니다.&lt;br /&gt;이제와서 고백하는데 프록시 JDBC라는 말은 없습니다. 정확히 말하면 Datsource를 프록시화한거지만요..&lt;br /&gt;사실 이 기능은 라이브러리를 사용하면 조금더 편한다고는 하는데 정확하게 이 코드에 대해 설명하기 위해서는 라이브러리를 사용하지 않는것이 좋다고 판단하였습니다.&lt;br /&gt;아무튼 이렇게 정리를 해보니 눈에 보이는것같습니다. 리플렉션 기술은 무분별하게 사용하면 안된다고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 된다면 jdk 동적 프록시 vs cglib와 레플렉션에 대해 깊게 학습하는 시간을 가져봅시다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/546</guid>
      <comments>https://b-programmer.tistory.com/546#entry546comment</comments>
      <pubDate>Mon, 9 Mar 2026 18:27:01 +0900</pubDate>
    </item>
    <item>
      <title>프록시 JDBC(1)</title>
      <link>https://b-programmer.tistory.com/545</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 저는 파일을 통해 slow쿼리를 계산을 진행하였습니다. 하지만 그러다 보니 실시간으로 메시지를 보여주지 못하였습니다. 처음 의도가 네트워크 상황이라던지 여러 요인들이 존재하기 때문에 파일을 읽는 것을 선택하였지만, 곰곰히 생각해보니 그것을 어플리케이션 쪽에서 파일을 읽을 필요는 없었습니다. 어플리케이션에서 slow쿼리를 확인할때 그것이 네트워크 이슈나 다른 이슈들이 섞여있지 않다는 것을 확인할 필요가 없었습니다. 만약, db쪽에 slow쿼리 이슈라면 파일만 만들어두고 확인하는 방법을 선택하면 되었기 때문입니다. 그래서 slow쿼리를 파일로 읽는 방식이 아닌 datasource를 이용해서 어플리케이션에서 읽는 방법을 선택하였습니다. 중요한 사실은 datasorce에서시간 측정하는 것은 불가능합니다. 이것을 가능하게 하는것이 프록시입니다. 이 파트에서 설명할 주제는 프록시 jdbc입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프록시 JDBC는 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 JDBC를 알기전에 프록시가 무엇인지 부터 알아보는게 좋을거 같습니다.&lt;br /&gt;프록시는 대리인이라는 뜻을 가지고 있습니다. 즉, 프록시 JDBC는 JDBC를 대신해서 무언가를 처리를 해주는 것을 의미합니다.&lt;br /&gt;그렇다면 프록시 JDBC는 어떻게 하면 만들 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Java 애플리케이션이 JDBC를 통해 데이터베이스에 어떻게 접근하는지 살펴보겠습니다. Java에서는 데이터베이스와 직접 연결하기 위해 JDBC(Java Database Connectivity) 를 사용합니다. 일반적으로 애플리케이션은 JDBC를 사용할 때 DataSource를 통해&amp;nbsp; Connection을 획득하게 됩니다. DataSource는 데이터베이스 연결 정보를 관리하고, 애플리케이션이 데이터베이스와 연결할 수 있도록 Connection을 제공하는 객체입니다.&lt;/p&gt;
&lt;p data-end=&quot;472&quot; data-start=&quot;429&quot; data-ke-size=&quot;size16&quot;&gt;즉, Java 애플리케이션에서 데이터베이스에 접근하는 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772967081522&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DataSource &amp;rarr; Connection &amp;rarr; PreparedStatement &amp;rarr; execute&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC를 대신해서 시간을 측정을 하는 것을 만들 기 위해서는 execute쪽에 프록시 객체를 만들면 되지 않을까요?&amp;nbsp;&lt;br /&gt;이러한 방식이라면 Service나 Repository 메서드 전체 시간이 아니라, 실제로 실행된 SQL 단위로 정확한 실행 시간을 측정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 execute만 프록시 객체로 만들어서 사용하기는 어렵다고 합니다. 그 이유가 무엇일까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;execute만 프록시 객체로 만들어서 사용하기는 어려운 이유?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1629&quot; data-start=&quot;1575&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 생성 지점입니다. PreparedStatement는 Connection.prepareStatement()를 통해 생성되어 즉시 호출자에게 반환됩니다. 따라서 이 반환 과정에 개입하지 못하면 원본 객체를 프록시 객체로 치환할 수 없습니다. 결국 자동 적용을 위해서는 Connection이나 DataSource를 래핑하거나, 더 아래 계층인 JDBC 드라이버 수준에서 개입해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;420&quot; data-start=&quot;298&quot; data-ke-size=&quot;size16&quot;&gt;결국은 execute를 실행하기 위해서는 PreparedStatement가 필요하고,&lt;br /&gt;PreparedStatement를 생성하기 위해서는 Connection을 필요하고,&lt;br /&gt;Connection은 DataSource를 통해 생성되어집니다. 결국 DataSource부터 Wrapper 객체로 만들어야 합니다.&lt;br /&gt;결국 객체 생성 흐름의 시작점인 DataSource 단계부터 프록시를 적용해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;420&quot; data-start=&quot;298&quot; data-ke-size=&quot;size16&quot;&gt;다행스럽게도 스프링은 DataSource를 Wrapper 객체를 제공해주고 있네요. 바로 DelegatingDataSource입니다.&lt;br /&gt;그렇다면 DelegatingDataSource가 어떤것인지 알아봅시다.&lt;/p&gt;
&lt;h2 data-end=&quot;420&quot; data-start=&quot;298&quot; data-ke-size=&quot;size26&quot;&gt;DelegatingDataSource는 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DelegatingDataSource는 Spring에서 제공하는 DataSource Wrapper 클래스입니다.&lt;br /&gt;이 클래스의 역할은 기존 DataSource를 감싸서 호출을 위임(delegate)하면서 필요한 동작을 확장할 수 있도록 하는 것입니다.&lt;br /&gt;즉, DelegatingDataSource 자체가 특별한 로직을 수행하는 것은 아니고, 기존 DataSource를 감싸는 기본 구조를 제공하는 클래스라고 볼 수 있습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1727&quot; data-start=&quot;1640&quot; data-ke-size=&quot;size23&quot;&gt;그렇다면 DelegatingDataSource로 무엇을 할 수 있을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, DataSource를 그대로 두면서, 중간에 원하는 동작을 추가할 수 있게 해줍니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 getConnection()이 호출될 때 &lt;/span&gt;로그를 남기는 작업, 반환되는 Connection을 감싸는 작업,추가 설정을 넣는 작업&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;같은 작업들을 할 수 있습니다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;쉽게 말해서, &lt;/span&gt;기존 DataSource를 바꾸지 않고 확장할 수 있게 해주는 Wrapper라고 보면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 짚고 넘어가야 할 부분이 있습니다. 바로 반환되는 Connection을 감싸는 작업과 추가 설정을 넣는 작업입니다.&lt;br /&gt;얘네가 왜 중요하냐면&lt;/p&gt;
&lt;pre id=&quot;code_1772977821613&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;DataSource &amp;rarr; Connection &amp;rarr; PreparedStatement &amp;rarr; execute&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름에서 다음 단계로 가기 위해서는 Connection을 감싸야 하기 때문이죠.&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;br /&gt;오늘은 조금 더 쉬고 싶어서 내일로 미루려고 합니다.&lt;br /&gt;다음 글에서는 프록시 객체로 만들 수 있는지 확인해봅시다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/545</guid>
      <comments>https://b-programmer.tistory.com/545#entry545comment</comments>
      <pubDate>Sun, 8 Mar 2026 22:52:25 +0900</pubDate>
    </item>
    <item>
      <title>분명... slow쿼리를 모니터링하려고 했는데... 어째서...</title>
      <link>https://b-programmer.tistory.com/544</link>
      <description>&lt;pre id=&quot;code_1772790888741&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; return wrapPreparedStatement(preparedStatement, sql);​&lt;/code&gt;&lt;/pre&gt;
&lt;figure id=&quot;og_1772772044085&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;슬로쿼리 모니터링 인터페이스 제작&quot; data-og-description=&quot;슬로 쿼리를 모니터링 인터페이스를 만들기위해 어떤 작업을 해야 할까요? slow 쿼리는 어떻게 탐지할 수 있을까?Slow Query는 단순히 실행 시간이 길었던 쿼리를 의미하는 것이 아니라, 데이터베이&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/524&quot; data-og-url=&quot;https://b-programmer.tistory.com/524&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bO7BLZ/dJMb8Z3o8m9/wxDkxqJsRCaYdaPdseRLy0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/byURjN/dJMb8T9Xk4R/HRxCLmVNeok8u1NDOD4Ok1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bInmG5/dJMb8TB7sM7/6LYecm8kkIKdxoipY6r741/img.jpg?width=383&amp;amp;height=248&amp;amp;face=108_72_174_144&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/524&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/524&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bO7BLZ/dJMb8Z3o8m9/wxDkxqJsRCaYdaPdseRLy0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/byURjN/dJMb8T9Xk4R/HRxCLmVNeok8u1NDOD4Ok1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bInmG5/dJMb8TB7sM7/6LYecm8kkIKdxoipY6r741/img.jpg?width=383&amp;amp;height=248&amp;amp;face=108_72_174_144');&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;슬로쿼리 모니터링 인터페이스 제작&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;슬로 쿼리를 모니터링 인터페이스를 만들기위해 어떤 작업을 해야 할까요? slow 쿼리는 어떻게 탐지할 수 있을까?Slow Query는 단순히 실행 시간이 길었던 쿼리를 의미하는 것이 아니라, 데이터베이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모니터링 서비스를 만들면서, DB가 슬로우 쿼리로 판단한 요청을 로그나 알람 형태로 수집하고 보여주는 기능을 구현하고자 했습니다. 초기에는 long_query_time 값을 의도적으로 매우 낮게 설정한 뒤 테스트를 진행했고, 이때는 스케줄링 방식으로 실행했을 때도 비교적 정상적으로 슬로우 로그가 기록되는 것을 확인할 수 있었습니다. 하지만 실제 운영 환경을 가정해 long_query_time을 1~2초 수준으로 조정한 뒤 다시 테스트했을 때는 기대와 달리 slow.log가 기록되지 않았습니다. 조회한 쿼리가 단순한 SELECT 문이었기 때문일 수도 있지만, 현재로서는 그 이유를 명확히 판단하지 못했습니다. 이 부분은 추가적인 확인이 필요하다고 생각합니다. 그래서 데이터를 의도적으로 크게 늘려 보면서 다시 테스트했습니다. 100만 건, 1000만 건, 그 이상까지 확장하려 했고, 최종적으로는 1억 건 수준까지 시도하고자 했습니다. 다만 도커 환경의 디스크 용량 문제로 인해 더 이상 데이터를 적재할 수 없었고, 확인해보니 약 4000만 건 정도까지 입력된 상태였습니다.이 정도로 데이터를 늘렸음에도 불구하고 SQL 자체의 처리 시간은 여전히 1초 안팎이었습니다. 이 결과를 보면 데이터 양이 아직 충분하지 않았다고 볼 수도 있고, 혹은 단일 테이블 조회였기 때문에 생각보다 빠르게 처리된 것일 수도 있습니다. 이러한 해석 모두 완전히 틀렸다고 보기는 어렵습니다. 다만 흥미로운 점은 API 전체 조회 속도는 1초 이상이 소요되었다는 점입니다. 즉, 실제 체감 성능은 느렸지만, DB 쿼리 자체는 슬로우 쿼리로 분류되지 않았습니다. 앞으로는 많은 데이터가 들어왔음에도 왜 쿼리 실행 시간이 1초 이내로 유지되었는지 학습해보고, 슬로우 쿼리를 어떤 방식으로 효과적으로 수집하고 보여줄 수 있을지 더 고민해볼 예정입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1000만건의 데이터도 1초안에 쿼리가 실행되어지는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 실행이 1초안에 발생하는 이유는 여러가지가 있었습니다. 일단 제가 사용했던 쿼리를 먼저 살펴보겠습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Page&amp;lt;Post&amp;gt; findAllByDeletedAtIsNull(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;386&quot; data-start=&quot;372&quot; data-section-id=&quot;176cc9u&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;이것을 쿼리로 생각해보면 다음과 같습니다.&lt;br /&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1772775945664&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM post
WHERE deleted_at IS NULL
LIMIT 10000000;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제일 큰 이유는 조건이 매무 단순하다는 것입니다.&lt;br /&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;delete_at이 null인 조건은 DB가 처리하기에 복잡한 조건이 아닙니다.&amp;nbsp;&lt;br /&gt;그렇다면 어떤 조건들이 복잡한 조건인지 살펴보겠습니다.&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;DB가 생각하는 &lt;u&gt;복잡한 조건&lt;/u&gt;에 대해 알아봅시다.&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 말하는 복잡한 조건은 단순히 조건식이 길다는 의미가 아니라, DB가 데이터를 찾거나 계산하는 과정에서 추가적인 작업이 많이 필요한 경우를 의미합니다. 즉, &lt;u&gt;한 row를 읽고 바로 판단할 수 있는 조건&lt;/u&gt;이 아니라 &lt;u&gt;추가 연산, 정렬, 집계, 탐색 비용이 발생하는 조건&lt;/u&gt;을 말합니다.&lt;/p&gt;
&lt;p data-end=&quot;438&quot; data-start=&quot;406&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫 번째는 컬럼에 함수나 연산이 적용된 경우입니다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1772776993068&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE LOWER(name) = 'john'
WHERE DATE(created_at) = '2025-01-01'
WHERE price * 1.1 &amp;gt; 100&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;599&quot; data-start=&quot;541&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 &lt;u&gt;DB는 값을 읽은 뒤 함수나 연산을 수행해야 하므로 모든 row에 대해 추가 계산이 발생&lt;/u&gt;합니다.&lt;/p&gt;
&lt;p data-end=&quot;637&quot; data-start=&quot;601&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 번째는 패턴 검색이나 문자열 탐색이 필요한 경우입니다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1772777077649&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE name LIKE '%kim'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;735&quot; data-start=&quot;674&quot; data-ke-size=&quot;size16&quot;&gt;앞에 %가 붙은 검색은&lt;u&gt; B-Tree 인덱스를 활용하기 어렵기 때문에 대부분 테이블 전체를 검사&lt;/u&gt;해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;761&quot; data-start=&quot;737&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세 번째는 정렬이 필요한 경우입니다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1772777190476&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM post
ORDER BY created_at DESC&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;897&quot; data-start=&quot;819&quot; data-ke-size=&quot;size16&quot;&gt;정렬을 수행하려면 DB는 &lt;u&gt;데이터를 읽은 뒤 메모리에 모아서 정렬 작업을 수행&lt;/u&gt;해야 합니다.&lt;br /&gt;데이터가 많을수록 정렬 비용이 크게 증가합니다.&lt;/p&gt;
&lt;p data-end=&quot;930&quot; data-start=&quot;899&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;네 번째는 집계나 그룹 연산이 필요한 경우입니다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1772777473428&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT author_id, COUNT(*)
FROM post
GROUP BY author_id&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1053&quot; data-start=&quot;1000&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 DB는 &lt;u&gt;데이터를 읽은 뒤 같은 값을 가진 row들을 묶고 집계 계산을 수행&lt;/u&gt;해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;1085&quot; data-start=&quot;1055&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다섯 번째는 여러 테이블을 결합하는 경우입니다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre id=&quot;code_1772777483947&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM post p
JOIN user u ON p.user_id = u.id&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1233&quot; data-start=&quot;1152&quot; data-ke-size=&quot;size16&quot;&gt;JOIN이 들어가면 DB는 한 테이블의 데이터를 읽으면서 다른 테이블에서 매칭되는 데이터를 계속 찾아야 하기 때문에 처리 과정이 더 복잡해집니다.&lt;/p&gt;
&lt;p data-end=&quot;1262&quot; data-start=&quot;1235&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;여섯 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;서브쿼리가 포함된 경우입니다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1772777500027&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM post
WHERE user_id IN (
SELECT id FROM user WHERE age &amp;gt; 30
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1416&quot; data-start=&quot;1353&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 DB는 내부적으로 서브쿼리를 실행하거나 결과를 별도로 처리해야 하므로 실행 계획이 복잡해질 수 있습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;정리하면, SQL에서 &lt;/span&gt;복잡한 조건&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이란 단순히 값 비교만 하는 것이 아니라 &lt;/span&gt;추가적인 계산, 정렬, 그룹핑, 테이블 결합 등의 작업이 필요한 경우&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;를 의미합니다. &lt;/span&gt;이러한 작업이 많아질수록 DB가 수행해야 하는 연산량과 I/O 비용이 증가하게 됩니다.&lt;br /&gt;따라서 데이터베이스 성능에서는 단순히 데이터의 개수보다, 쿼리가 수행하는 작업의 종류와 복잡도가 더 큰 영향을 미칩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;순차적으로 읽었을 가능성이 매우 크다.&lt;/h3&gt;
&lt;p data-end=&quot;671&quot; data-start=&quot;606&quot; data-ke-size=&quot;size16&quot;&gt;PK만 잡혀 있고 deleted_at 인덱스가 없다면, DB는 보통 테이블을 순서대로 읽으면서 조건을 검사합니다.&lt;br /&gt;이게 중요한 이유는 순차 읽기(sequential scan) 가 생각보다 빠르기 때문입니다. DB가 힘들어하는 건 여기저기 점프하는 랜덤 I/O인데,&lt;br /&gt;지금은 단일 테이블을 앞에서부터 쭉 읽는 형태라면 오히려 효율적일 수 있습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;1000만 건을 복잡하게 찾는 것은 느릴 수 있지만, &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;1000만 건을 &quot;쭉 읽는 것&quot;은 생각보다 빠를 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;909&quot; data-start=&quot;887&quot; data-section-id=&quot;3y8t0&quot; data-ke-size=&quot;size23&quot;&gt;저장 장치와 캐시 영향이 크다&lt;/h3&gt;
&lt;p data-end=&quot;951&quot; data-start=&quot;910&quot; data-ke-size=&quot;size16&quot;&gt;1000만 건이어도 전부 디스크에서 매번 새로 읽는 게 아닐 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;985&quot; data-start=&quot;953&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;973&quot; data-start=&quot;953&quot; data-section-id=&quot;131n4xg&quot;&gt;InnoDB Buffer Pool&lt;/li&gt;
&lt;li data-end=&quot;985&quot; data-start=&quot;974&quot; data-section-id=&quot;1nf55dg&quot;&gt;OS 페이지 캐시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이들에 대해서는 이글에서는 다루지는 않습니다.)&lt;/p&gt;
&lt;p data-end=&quot;1051&quot; data-start=&quot;987&quot; data-ke-size=&quot;size16&quot;&gt;이런 곳에 이미 데이터가 올라와 있으면, 디스크가 아니라 메모리에서 읽게 됩니다. 그러면 속도가 확 빨라집니다.&lt;br /&gt;그래서 실험에서 특히 주의해야 하는 게, 처음 조회한 속도와 두 번째 이후 조회한 속도는 다를 수 있다는 점입니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1163&quot; data-start=&quot;1129&quot; data-section-id=&quot;1hkh9ct&quot; data-ke-size=&quot;size23&quot;&gt;row 크기가 작으면 1000만 건도 감당 가능하다&lt;/h3&gt;
&lt;p data-end=&quot;1221&quot; data-start=&quot;1164&quot; data-ke-size=&quot;size16&quot;&gt;1000만 건은 숫자만 보면 커 보이는데, 실제로는 row 하나가 얼마나 큰지도 중요합니다.&lt;br /&gt;예를 들어 컬럼이 몇 개 안 되고 TEXT, BLOB 같은 큰 컬럼이 없으면 한 페이지에 많은 row가 들어갑니다.&lt;br /&gt;그러면 DB는 적은 페이지 I/O로 많은 데이터를 읽을 수 있습니다.&lt;br /&gt;즉, 같은 1000만 건이어도 row가 가벼운 테이블은 빠를 수 있고 row가 무거운 테이블은 훨씬 느릴 수 있습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1437&quot; data-start=&quot;1404&quot; data-section-id=&quot;7fdvp1&quot; data-ke-size=&quot;size23&quot;&gt;DB 실행 시간과 애플리케이션 응답 시간은 다르다&lt;/h3&gt;
&lt;p data-end=&quot;1451&quot; data-start=&quot;1438&quot; data-ke-size=&quot;size16&quot;&gt;여기서 많이 헷갈립니다. DB는 1초 안에 끝났더라도, API는 더 오래 걸릴 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;1504&quot; data-start=&quot;1492&quot; data-ke-size=&quot;size16&quot;&gt;왜냐하면 API 쪽에는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1586&quot; data-start=&quot;1506&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1525&quot; data-start=&quot;1506&quot; data-section-id=&quot;1rbb0ew&quot;&gt;JDBC ResultSet 처리&lt;/li&gt;
&lt;li data-end=&quot;1538&quot; data-start=&quot;1526&quot; data-section-id=&quot;wod9ma&quot;&gt;JPA 엔티티 매핑&lt;/li&gt;
&lt;li data-end=&quot;1552&quot; data-start=&quot;1539&quot; data-section-id=&quot;1k2rvcw&quot;&gt;영속성 컨텍스트 관리&lt;/li&gt;
&lt;li data-end=&quot;1565&quot; data-start=&quot;1553&quot; data-section-id=&quot;1k7mxi6&quot;&gt;Page 객체 생성&lt;/li&gt;
&lt;li data-end=&quot;1576&quot; data-start=&quot;1566&quot; data-section-id=&quot;lo7ivd&quot;&gt;JSON 직렬화&lt;/li&gt;
&lt;li data-end=&quot;1586&quot; data-start=&quot;1577&quot; data-section-id=&quot;n70zrl&quot;&gt;네트워크 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1606&quot; data-start=&quot;1588&quot; data-ke-size=&quot;size16&quot;&gt;이런 비용이 추가되기 때문입니다.&lt;br /&gt;특히 1000만 건을 JPA 엔티티로 들고 오는 것은 DB보다 애플리케이션이 더 힘들 수 있습니다.&lt;br /&gt;그래서 DB slow log 는 안 찍혔지만, 그런데 API 는 느리는 상황이 충분히 나올 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 슬로우 쿼리라는 개념을 다시 정의할 필요가 있다고 생각합니다. 앞선 실험에서 확인했듯이, 데이터가 1000만 건 이상 존재하더라도 항상 느린 쿼리가 되는 것은 아니었습니다. 단순 조회의 경우 예상보다 빠르게 처리될 수 있으며, 이 경우 DB 입장에서는 슬로우 쿼리로 판단되지 않을 수도 있습니다. 하지만 이러한 상황에서도 API 응답 시간은 충분히 느려질 수 있습니다. 즉, 사용자는 느리다고 느끼지만 DB에서는 슬로우 쿼리가 기록되지 않는 상황이 발생할 수 있습니다. 이때 중요한 것은 어디에서 지연이 발생하는지 정확하게 파악하는 것이라고 생각합니다. 일반적으로 슬로우 쿼리를 확인하기 위해 DB에서 생성하는 slow.log 파일을 분석하는 것은 매우 자연스러운 접근 방식입니다. 하지만 만약 쿼리 자체는 빠르게 실행되었고, 실제 병목이 애플리케이션 로직이나 데이터 처리 과정에 있다면 어떻게 해야 할까요? 이 경우 단순히 DB의 슬로우 로그만 확인하는 방식으로는 문제를 파악하기 어렵습니다. 그렇다고 해서 애플리케이션 레벨에서만 느린 요청을 기준으로 판단하는 것도 정확한 접근이라고 보기는 어렵습니다. 결국 중요한 것은 DB, 애플리케이션, 네트워크 등 각 계층에서의 처리 시간을 구분하여 관찰하는 것입니다. 어디에서 실제 지연이 발생하는지를 명확하게 파악해야 보다 정확한 모니터링과 문제 분석이 가능하다고 생각합니다.&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;1026&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhIiPR/dJMcafeIVmX/naYBadgzk88ZNvKaSGFaY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhIiPR/dJMcafeIVmX/naYBadgzk88ZNvKaSGFaY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhIiPR/dJMcafeIVmX/naYBadgzk88ZNvKaSGFaY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhIiPR%2FdJMcafeIVmX%2FnaYBadgzk88ZNvKaSGFaY1%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;623&quot; height=&quot;192&quot; data-origin-width=&quot;1026&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 DB에서 슬로우 쿼리는 1초로 설정하겠습니다.&lt;br /&gt;앞으로 DB에서 발생한 슬로우 쿼리는 단순히 느린 요청이 아니라, &lt;u&gt;쿼리 자체의 성능이 떨어졌다는 신호로 보고 쿼리를 수정하거나 튜닝해야 하는 대상으로 사용&lt;/u&gt;해야 할 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;slow.log파일도 초기화를 시켜뒀습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1104&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhwuEX/dJMcacbceT6/KjaXYkIuMId47LpdfXldp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhwuEX/dJMcacbceT6/KjaXYkIuMId47LpdfXldp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhwuEX/dJMcacbceT6/KjaXYkIuMId47LpdfXldp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhwuEX%2FdJMcacbceT6%2FKjaXYkIuMId47LpdfXldp0%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;766&quot; height=&quot;154&quot; data-origin-width=&quot;1104&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터는 1000만건 정도 들어가있는 상태입니다. 이제 postman으로 돌려보겠습니다. (스웨거로 돌리고 싶었지만 시간체크를 하지 않나요..ㅜㅜ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 api 조회 성능이 나쁘지 않네요. 이러면 어떻게 해야할지... 당황스럽네요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2292&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r04tB/dJMcaflsjga/JAi0GCEu0LS2UbyGnckq80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r04tB/dJMcaflsjga/JAi0GCEu0LS2UbyGnckq80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r04tB/dJMcaflsjga/JAi0GCEu0LS2UbyGnckq80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr04tB%2FdJMcaflsjga%2FJAi0GCEu0LS2UbyGnckq80%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;2292&quot; height=&quot;496&quot; data-origin-width=&quot;2292&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단, 마음 추수리고 그럼에도 불구하고 앱에서도 측정하는 방법을 추가하였습니다.&lt;br /&gt;처음에는 AOP를 이용하는 방법을 생각했었는데 AOP말고 인터셉터로 사용할 수 있는 방법이 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AOP가 아닌 인터셉터로 구현한 이유?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP를 사용하면 일반적으로 Repository 나 Service 메서드의 실행 시간을 측정하게 됩니다. 하지만 이렇게 측정한 시간은 실제 SQL 1건의 실행 시간이라기보다, 메서드 전체 수행 시간이 함께 포함된 값입니다. 예를 들어 하나의 서비스 메서드 안에서 여러 개의 SQL이 실행될 수 있습니다. 이 경우 AOP로는 해당 메서드가 얼마나 오래 걸렸는지는 알 수 있지만, 어떤 쿼리가 느렸는지, 혹은 각 쿼리가 얼마나 걸렸는지를 정확하게 구분하기 어렵습니다.&lt;br /&gt;또한 JPA를 사용하는 경우에는 지연 로딩과 같이 개발자가 직접 작성하지 않은 SQL이 내부적으로 추가 실행될 수 있습니다.&lt;br /&gt;이러한 쿼리들은 메서드 단위 측정만으로는 추적이 부정확해질 수 있습니다.&lt;br /&gt;반면 현재처럼 DataSource 또는 Statement 레벨을 가로채는 방식은, 실제 JDBC execute 시점을 기준으로 동작합니다.&lt;br /&gt;즉, 애플리케이션 메서드가 아니라 실제로 실행된 SQL 단위로 시간을 측정할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;795&quot; data-start=&quot;655&quot; data-ke-size=&quot;size16&quot;&gt;결국 슬로우 쿼리 모니터링의 목적이 &quot;어떤 서비스 메서드가 느린가&quot;를 찾는 것이 아니라 &quot;어떤 SQL이 실제로 느리게 수행되었는가&quot;를 식별하는 것이라면, AOP보다 인터셉터 기반의 데이터소스 프록시 방식이 더 적절하다고 판단했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DataSource 또는 Statement 레벨 가로채기&lt;/h4&gt;
&lt;pre id=&quot;code_1772788714617&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AppDbLatencyDataSource extends DelegatingDataSource&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스를 사용하게 되면 Datasource를 유지하면서 새로운 기능으로 확장이 가능하다고 합니다.&lt;br /&gt;데이터베이스 성능을 이해하려면 애플리케이션과 데이터베이스가 어떻게 통신하는지부터 생각해볼 필요가 있습니다.&lt;br /&gt;애플리케이션은 데이터베이스와 직접 데이터를 주고받으며 동작하기 때문입니다. 먼저 애플리케이션과 데이터베이스가 어떤 방식으로 연결되는지 살펴보겠습니다. 일반적으로 데이터베이스와 애플리케이션 간의 통신은 TCP 기반 네트워크 프로토콜을 통해 이루어집니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, 애플리케이션에서 쿼리를 실행하면 내부적으로는 TCP 연결을 통해 데이터베이스 서버로 요청이 전달되고, 데이터베이스는 쿼리를 처리한 뒤 결과를 다시 TCP를 통해 애플리케이션으로 반환합니다.&lt;br /&gt;&lt;/span&gt;이 과정에서 단순히 SQL을 실행하는 것뿐만 아니라 네트워크 전송, 결과 데이터 반환, 데이터 파싱 등의 과정이 함께 이루어지게 됩니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;따라서 데이터베이스 성능을 분석할 때는 단순히 SQL 실행 시간뿐만 아니라, &lt;/span&gt;애플리케이션과 데이터베이스 간의 통신 과정도 함께 고려할 필요가 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터베이스와 통신하기 위해서는 Connection을 통해 연결을 맺어야 합니다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection은 애플리케이션과 데이터베이스 사이에서 실제로 데이터를 주고받는 대화 채널(세션) 역할을 합니다.&lt;br /&gt;Connection을 통해 다음과 같은 작업을 수행하게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;434&quot; data-start=&quot;311&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;348&quot; data-start=&quot;311&quot; data-section-id=&quot;1dwpg9b&quot;&gt;SQL 실행 객체(PreparedStatement) 생성&lt;/li&gt;
&lt;li data-end=&quot;383&quot; data-start=&quot;349&quot; data-section-id=&quot;h2eewr&quot;&gt;트랜잭션 제어 (commit, rollback)&lt;/li&gt;
&lt;li data-end=&quot;409&quot; data-start=&quot;384&quot; data-section-id=&quot;16onwsc&quot;&gt;자동 커밋 및 트랜잭션 격리 수준 설정&lt;/li&gt;
&lt;li data-end=&quot;434&quot; data-start=&quot;410&quot; data-section-id=&quot;1ti56yy&quot;&gt;데이터베이스와의 실제 연결 유지 및 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;465&quot; data-start=&quot;436&quot; data-ke-size=&quot;size16&quot;&gt;일반적으로 SQL이 실행되는 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772789690525&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Connection &amp;rarr; PreparedStatement &amp;rarr; execute&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Connection을 통해 데이터베이스와 연결을 맺고, 그 다음 PreparedStatement를 생성하여 실행할 SQL을 준비합니다.&lt;br /&gt;마지막으로 execute()를 호출하면서 실제로 SQL이 데이터베이스로 전달됩니다. 여기서 우리가 주목해야 하는 부분은 바로 execute() 호출 시점입니다. 이 시점에서 애플리케이션은 실제로 SQL을 데이터베이스로 보내게 되고, 데이터베이스는 쿼리를 실행한 뒤 그 결과를 다시 애플리케이션으로 반환합니다. &lt;br /&gt;&lt;u&gt;즉, SQL 실행 시간을 정확하게 측정하려면 메서드나 비즈니스 로직 단위가 아니라, 실제 JDBC execute()가 호출되는 시점을 기준으로 측정해야 합니다.&lt;/u&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Connection에 대해서는 어느 정도 이해가 되었으니, 이제 PreparedStatement와 execute가 무엇인지 생각해볼 필요가 있습니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 PreparedStatement의 역할을 이해하는 것이 중요합니다. PreparedStatement는 실제로 실행할 SQL을 준비하는 객체입니다.&lt;br /&gt;애플리케이션은 Connection을 통해 PreparedStatement를 생성하고, 그 안에 실행할 SQL을 설정하게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;341&quot; data-start=&quot;311&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 코드가 있다고 가정해보겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1772790273417&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PreparedStatement ps = connection.prepareStatement(
    &quot;SELECT * FROM post WHERE id = ?&quot;
);
ps.setLong(1, 10);
ps.executeQuery();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 먼저 prepareStatement()를 호출하면서 SQL이 준비(prepared) 됩니다.&lt;br /&gt;이 단계에서는 SQL 구조가 데이터베이스에 전달되고, 실행 계획을 준비하는 과정이 이루어질 수 있습니다.&lt;br /&gt;그 다음 setLong()과 같은 메서드를 통해 실제 파라미터 값을 바인딩합니다.&lt;/p&gt;
&lt;p data-end=&quot;768&quot; data-start=&quot;663&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 execute() 또는 executeQuery()가 호출되는 순간, &lt;br /&gt;PreparedStatement에 준비된 SQL이 실제로 데이터베이스에 전달되고 실행됩니다.&lt;br /&gt;즉, 흐름을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772790297241&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Connection &amp;rarr; PreparedStatement 생성 &amp;rarr; 파라미터 바인딩 &amp;rarr; execute() 호출 &amp;rarr; SQL 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 실제 SQL 실행은 execute() 시점에 발생한다는 것입니다. 따라서 SQL 실행 시간을 정확하게 측정하려면 Service나 Repository 메서드가 아니라, JDBC의 execute()가 호출되는 시점을 기준으로 측정하는 것이 더 정확합니다. &lt;br /&gt;이러한 이유로 슬로우 쿼리를 측정할 때는 AOP보다는 DataSource 또는 Statement 레벨을 가로채는 방식이 더 적절하다고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주목해야 하는 사실이 우리는 쿼리의 시간을 측정하는것이 목표입니다. 그렇기 때문에 만약 preparedStatement이 아니라면 시간 측정 대상이 아니기 때문에 결과를 그대로 반환 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772790659562&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (!&quot;prepareStatement&quot;.equals(methodName) &amp;amp;&amp;amp; !&quot;prepareCall&quot;.equals(methodName)) {
  return result;
}

if (!(result instanceof PreparedStatement preparedStatement)) {
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모든 sql문이 preparedStatement인 코드만 모여졌습니다.&lt;br /&gt;다시 프록시로 감싸겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772790896532&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return wrapPreparedStatement(preparedStatement, sql);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 Connection과 PreparedStatement, 그리고 execute() 호출 시점을 살펴보았으니, 이제는 실제로 SQL 실행에 얼마의 시간이 소요되는지 측정해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772791449835&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String traceId = resolveTraceId();
long startedNanos = System.nanoTime();

try {
  Object result = invoke(target, method, args);
  monitor.collect(sql, traceId, toElapsedMillis(startedNanos));
  return result;
} catch (Throwable throwable) {
  monitor.collect(sql, traceId, toElapsedMillis(startedNanos));
  throw throwable;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;traceId는 MDC에서 읽은 값을 사용하였습니다. 자세한 내용은 다음글을 참고해주세요.&lt;/p&gt;
&lt;figure id=&quot;og_1772791553710&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;MDC란 무엇인가 그리고 어떻게 설정할 수 있을까?&quot; data-og-description=&quot;MDC는 ThreadLocal에 로그 컨텍스트를 저장해두고, logframwork가 자동으로 꺼내 쓰게 만드는 장치라고 합니다. 위의 메시지로 그려보면 위와 같은 그림이 그려집니다. 그렇다면 우리는 어떤것을 학습&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/498&quot; data-og-url=&quot;https://b-programmer.tistory.com/498&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/beQlsB/dJMb9dHlXl7/P8vKj456aeOK7jZKok8opk/img.png?width=800&amp;amp;height=318&amp;amp;face=0_0_800_318,https://scrap.kakaocdn.net/dn/byySZ4/dJMb83SgIQc/zxLA1FyYA8GqAcMIA29aO0/img.png?width=800&amp;amp;height=318&amp;amp;face=0_0_800_318,https://scrap.kakaocdn.net/dn/boYW3r/dJMb9kT0Sll/pZaXSMsqAJzBCHetDyeaBK/img.png?width=1966&amp;amp;height=782&amp;amp;face=0_0_1966_782&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/498&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/498&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/beQlsB/dJMb9dHlXl7/P8vKj456aeOK7jZKok8opk/img.png?width=800&amp;amp;height=318&amp;amp;face=0_0_800_318,https://scrap.kakaocdn.net/dn/byySZ4/dJMb83SgIQc/zxLA1FyYA8GqAcMIA29aO0/img.png?width=800&amp;amp;height=318&amp;amp;face=0_0_800_318,https://scrap.kakaocdn.net/dn/boYW3r/dJMb9kT0Sll/pZaXSMsqAJzBCHetDyeaBK/img.png?width=1966&amp;amp;height=782&amp;amp;face=0_0_1966_782');&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;MDC란 무엇인가 그리고 어떻게 설정할 수 있을까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;MDC는 ThreadLocal에 로그 컨텍스트를 저장해두고, logframwork가 자동으로 꺼내 쓰게 만드는 장치라고 합니다. 위의 메시지로 그려보면 위와 같은 그림이 그려집니다. 그렇다면 우리는 어떤것을 학습&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 monitor를 통해 기록을 하도록 하였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이제 모니터...&lt;/h2&gt;
&lt;pre id=&quot;code_1772791719690&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (!enabled || elapsedMillis &amp;lt; slowThresholdMillis) {
  return;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 설정된 시간보다 더 적게 걸린다면, 슬로우 쿼리가 아니기 때문에 무시를 하고&lt;br /&gt;그렇지 않는 경우에는 적절한 파싱을 거친뒤, 어떻게 보여줄지에 대한 핸들러를 만들었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772791884221&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public AppDbLatencyEventHandler appDbLatencyEventHandler() {
  return event -&amp;gt; {
    log.info(&quot;앱 슬로쿼리 감지: queryTime={}초, sql={}&quot;, event.queryTime(), event.sql());
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2392&quot; data-origin-height=&quot;144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E975I/dJMcaaEtgkS/uKAYf1gMbyMg9PQALyOUs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E975I/dJMcaaEtgkS/uKAYf1gMbyMg9PQALyOUs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E975I/dJMcaaEtgkS/uKAYf1gMbyMg9PQALyOUs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE975I%2FdJMcaaEtgkS%2FuKAYf1gMbyMg9PQALyOUs1%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;2392&quot; height=&quot;144&quot; data-origin-width=&quot;2392&quot; data-origin-height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 의도자체는 DB에서 읽어서 보여주는 것이 조금더 합리적이라고 판단하였습니다. 하지만 외부 환경에서 파일을 읽는 작업이다보니 추가적인 리소스가 발생하여 자주 사용하지 못한다는 단점을 가지고 있었습니다. 그럼에도 불구하고 진행했던 이유는 실시간으로 보여줄 필요가 없을거 같다 느껴져 진행했었습니다. 하지만 1000만건의 데이터가 들어갔음에도 불구하고 slow.log에 로그가 찍히지않는 문제가 발생하였습니다. 엎친데 덮친격으로 스웨거에서는 속도가 빠르게 나오지는 않았습니다. 체감상 2~3s정도 걸렸던거 같습니다. 그래서 애플리케이션측면에서도 슬로우 쿼리를 측정하는것이 나쁘지 않다고 판단하였습니다. 아무리 슬로우 쿼리일지라도 어디에서 느린 성능을 보이는지 정확하게 판단이 되어진다면, 조금더 빠른 대처가 가능하다고 생각하였습니다. 만들다보니 하나 안 사실이 있는데 애플리케이션은 실시간으로 보여줄수 있었습니다. 즉,&amp;nbsp; DB에서 읽었을때 보다 빠르게 대처가 가능하였습니다. 지금 생각해보면 DB에서 파일을 읽어서 가져올 필요없이 DB에는 설정만해놓고 애플리케이션에서 확인하고 만약에 로그가 발생하였다면, 그때 DB의 slow.log파일을 읽어보는 것이 합당하지 않나 여겨집니다. (참고로 스웨거에서는 느렸지만 포스트맨으로 확인해보니 50s안쪽으로 들어왔었습니다.)&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/544</guid>
      <comments>https://b-programmer.tistory.com/544#entry544comment</comments>
      <pubDate>Fri, 6 Mar 2026 19:27:23 +0900</pubDate>
    </item>
    <item>
      <title>데이터베이스의 종류</title>
      <link>https://b-programmer.tistory.com/543</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 데이터를 저장하고 관리하기 위한 인프라입니다. 일반적으로 데이터베이스라고 하면 가장 먼저 관계형 데이터베이스(RDB)를 떠올리는 경우가 많습니다. 하지만 최근에는 관계형 데이터베이스뿐만 아니라 NoSQL 데이터베이스도 함께 사용하는 경우가 많아졌습니다. 특히 애플리케이션의 요구사항에 따라 서로 다른 데이터베이스를 조합하여 사용하는 사례도 점점 늘어나고 있습니다. 또한 데이터베이스의 종류는 RDB와 NoSQL만 존재하는 것이 아닙니다. 각각의 목적과 특성에 따라 다양한 데이터베이스 유형이 존재합니다.&lt;br /&gt;이번 글에서는 이러한 다양한 데이터베이스 유형에 대해 함께 알아보겠습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1772716841721&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;RDB vs Nosql&quot; data-og-description=&quot;크게 DB에는 2가지 종류가 존재합니다. SQL을 사용하는 RDB와 SQL도 사용하는 NoSql(NoSql이 왜 Nosql인지에 대해 sql을 안써서 noSql이다 아니다 Not only sql의 약자라 그렇다라는 말들이 존재합니다. 안 중&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/485&quot; data-og-url=&quot;https://b-programmer.tistory.com/485&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bazWj8/dJMb8RRPIoP/3hzFQi9bdurf6zMhqsKQrk/img.jpg?width=800&amp;amp;height=425&amp;amp;face=0_0_800_425,https://scrap.kakaocdn.net/dn/w0plG/dJMb8YXJjt4/M8OvLn4Jm4wKRavbaSk8uk/img.jpg?width=800&amp;amp;height=425&amp;amp;face=0_0_800_425,https://scrap.kakaocdn.net/dn/RzmpK/dJMb8YpTfAB/t0MUHOyFb4kUoh76K1Z4K0/img.jpg?width=205&amp;amp;height=172&amp;amp;face=0_0_205_172&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/485&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/485&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bazWj8/dJMb8RRPIoP/3hzFQi9bdurf6zMhqsKQrk/img.jpg?width=800&amp;amp;height=425&amp;amp;face=0_0_800_425,https://scrap.kakaocdn.net/dn/w0plG/dJMb8YXJjt4/M8OvLn4Jm4wKRavbaSk8uk/img.jpg?width=800&amp;amp;height=425&amp;amp;face=0_0_800_425,https://scrap.kakaocdn.net/dn/RzmpK/dJMb8YpTfAB/t0MUHOyFb4kUoh76K1Z4K0/img.jpg?width=205&amp;amp;height=172&amp;amp;face=0_0_205_172');&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;RDB vs Nosql&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;크게 DB에는 2가지 종류가 존재합니다. SQL을 사용하는 RDB와 SQL도 사용하는 NoSql(NoSql이 왜 Nosql인지에 대해 sql을 안써서 noSql이다 아니다 Not only sql의 약자라 그렇다라는 말들이 존재합니다. 안 중&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 2가지만 소개했었네요. 그럼 빠르게 RDB랑 NoSQL정리하고 나머지도 학습해봅시다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가장 잘 알고 있는 관계형 데이터 베이스 RDB부터 알아봅시다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 익숙한 데이터베이스 유형인 관계형 데이터베이스(RDB)부터 살펴보겠습니다.&lt;br /&gt;관계형 데이터베이스에서 가장 중요한 특징 중 하나는 트랜잭션(Transaction)입니다. 트랜잭션은 ACID라는 특성을 기반으로 동작합니다. 이 글에서는 ACID에 대해 자세히 다루지는 않지만, 이러한 특성 덕분에 관계형 데이터베이스는 정확한 데이터 관리가 중요한 서비스에서 널리 사용됩니다. 예를 들어 데이터의 정확성과 일관성이 매우 중요한 영역에서 주로 활용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스의 주요 특징으로는 강한 데이터 정합성 보장과 복잡한 쿼리 처리 능력을 들 수 있습니다. 반면,&lt;u&gt; 수평 확장이 상대적으로 어렵고 스키마 변경이 까다롭다는 단점&lt;/u&gt;이 있습니다. 이말은 한번 결정하면 수정하기가 굉장히 까다롭다는 뜻이라고 할 수 있습니다.&lt;br /&gt;&lt;br /&gt;또한 구조화된 데이터에 최적화되어 있기 때문에 비정형 데이터나 반정형 데이터를 처리하는 데에는 한계가 있을 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 NoSQL은 어떨까?&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 DB의 확장성 문제를 해결하려고 등장하였습니다. 대표적으로 MongoDB, Cassandra, Redis, DynamoDB가 존재합니다.&lt;br /&gt;이들은 관계형 DB와 달리 테이블 구조가 아닌 다양한 형태로 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 4가지 유형이 존재합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Document DB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NoSQL 데이터베이스의 한 종류로, 데이터를 문서(Document) 형태로 저장하는 데이터베이스입니다. 여기서 말하는 문서는 일반적으로 JSON, BSON, XML 같은 반정형 데이터 구조를 의미합니다.&lt;br /&gt;대표적으로 MongoDB가 존재합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;반정형 데이터 구조란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반정형 데이터는 완전히 고정된 구조는 아니지만, 일정한 형태의 구조를 가지고 있는 데이터를 의미합니다.&lt;br /&gt;즉, 기본적인 구조는 존재하지만 필드가 자유롭게 추가되거나 변경될 수 있어 유연하게 데이터를 표현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 데이터들은 주로 JSON 형태로 표현되고 저장됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772718961929&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;user&quot;: {
    &quot;id&quot;: 1,
    &quot;name&quot;: &quot;name&quot;
  },
  &quot;status&quot;: &quot;active&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 JSON 구조 덕분에 Document DB에서는 하나의 문서 안에 다양한 형태의 데이터를 함께 저장할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Column DB&lt;/h3&gt;
&lt;p data-end=&quot;170&quot; data-start=&quot;83&quot; data-ke-size=&quot;size16&quot;&gt;NoSQL 데이터베이스의 한 종류로, 데이터를 행(Row)이 아닌 열(Column) 단위로 저장하는 데이터베이스입니다.&lt;br /&gt;관계형 데이터베이스는 &lt;u&gt;데이터를 행 단위로 저장&lt;/u&gt;하지만, Column DB는 &lt;u&gt;특정 컬럼을 중심으로 데이터를 저장하기 때문에 대량의 데이터를 조회하거나 분석하는 작업에 유리&lt;/u&gt;합니다. 이러한 특징 덕분에 대규모 데이터 처리나 분석 시스템에서 많이 사용됩니다. &lt;br /&gt;대표적인 Column DB로는 Apache Cassandra, HBase 등이 있습니다.&lt;/p&gt;
&lt;h4 data-end=&quot;170&quot; data-start=&quot;83&quot; data-ke-size=&quot;size20&quot;&gt;열로 저장을 한다는 것은 무슨 의미일까요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 RDB는 행으로 데이터를 저장합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772719287815&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[1, minzzi, 30]
[2, minsu, 28]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;170&quot; data-start=&quot;83&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Colunm DB의 저장 방식은 다릅니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772719341864&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;id   &amp;rarr; 1, 2
name &amp;rarr; minzzi, minsu
age  &amp;rarr; 30, 28&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;여기서 드는 의문은 이게 왜 대량의 데이터를 저장하는데 유리할까요?&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Column DB는 각 행마다 컬럼을 따로 두는 것이 아니라 같은 컬럼 데이터를 묶어서 저장하는 방식입니다.&lt;br /&gt;이러한 구조 덕분에 특정 컬럼을 조회할 때 더 효율적으로 데이터를 읽을 수 있습니다.&lt;br /&gt;예를 들어 age가 30 이상인 사람을 조회하는 상황을 생각해 보겠습니다.&lt;br /&gt;관계형 데이터베이스에서는 다음과 같은 쿼리를 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772719775061&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM users WHERE age &amp;gt;= 30&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스는 데이터를 행(Row) 단위로 저장하기 때문에, 실제로는 각 행의 데이터를 읽으면서 그 안에 있는 age 값을 확인하게 됩니다. 즉, name과 같은 다른 컬럼 데이터도 함께 읽게 됩니다.&lt;br /&gt;하지만 Column DB는 다릅니다. Column DB는 컬럼 단위로 데이터를 저장하기 때문에, age 컬럼 데이터만 읽어도 조건을 판단할 수 있습니다. 즉, 필요한 컬럼 데이터만 조회하면 되기 때문에 읽어야 하는 데이터의 양이 줄어들게 됩니다.&lt;br /&gt;이러한 이유로 Column DB는 대규모 데이터 분석이나 로그 처리와 같은 환경에서 높은 성능을 발휘합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Column DB는 데이터 개수가 다른 것이 아니라 저장 방식이 다르며, 특정 컬럼만 읽을 수 있어 분석 쿼리에 유리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Key-Value DB&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Key-Value DB는 NoSQL 데이터베이스의 한 종류로, 데이터를 Key와 Value의 쌍 형태로 저장하는 데이터베이스입니다.&lt;br /&gt;가장 단순한 형태의 데이터 구조를 가지고 있으며, 특정 Key를 통해 Value를 빠르게 조회할 수 있는 특징을 가지고 있습니다.&lt;br /&gt;예를 들어 다음과 같은 형태로 데이터를 저장할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772720017466&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;user:1 &amp;rarr; yonghun
user:2 &amp;rarr; minsu&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;u&gt;Key를 통해 데이터를 바로 조회할 수 있기 때문에 매우 빠른 읽기와 쓰기 성능을 제공&lt;/u&gt;합니다.&lt;br /&gt;이러한 특징 덕분에 Key-Value DB는 캐시, 세션 저장, 설정 정보 관리와 같은 용도로 많이 사용됩니다.&lt;br /&gt;대표적인 Key-Value DB로는 Redis, Amazon DynamoDB 등이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;graph DB&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;graph DB는 데이터 간의 관계(Relationship)를 저장하고 조회하는 데 특화된 데이터베이스입니다.&lt;br /&gt;일반적인 데이터베이스가 데이터를 테이블이나 문서 형태로 저장하는 것과 달리, Graph DB는 노드(Node)와 엣지(Edge) 구조를 통해 데이터를 표현합니다.&lt;br /&gt;예를 들어 다음과 같은 관계를 표현할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772720396687&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;사용자A &amp;rarr; 친구 &amp;rarr; 사용자B
사용자B &amp;rarr; 친구 &amp;rarr; 사용자C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;Node&lt;/b&gt; : 데이터를 나타내는 객체 &lt;b&gt;Edge&lt;/b&gt; : 객체 간의 관계를 의미합니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이러한 구조 덕분에 Graph DB는 &lt;/span&gt;데이터 간의 복잡한 관계를 빠르게 탐색할 수 있는 특징&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;을 가지고 있습니다.&lt;br /&gt;&lt;/span&gt;대표적인 Graph DB로는 Neo4j, Amazon Neptune 등이 있습니다.&lt;br /&gt;이러한 특징 때문에 Graph DB는 다음과 같은 서비스에서 많이 사용됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;578&quot; data-start=&quot;533&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;547&quot; data-start=&quot;533&quot;&gt;소셜 네트워크(SNS)&lt;/li&gt;
&lt;li data-end=&quot;556&quot; data-start=&quot;548&quot;&gt;추천 시스템&lt;/li&gt;
&lt;li data-end=&quot;568&quot; data-start=&quot;557&quot;&gt;사기 탐지 시스템&lt;/li&gt;
&lt;li data-end=&quot;578&quot; data-start=&quot;569&quot;&gt;네트워크 분석&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NoSQL 데이터베이스는 크게 Document DB, Column DB, Key-Value DB, Graph DB 네 가지 유형으로 나눌 수 있습니다.&lt;br /&gt;이러한 데이터베이스는 구조가 유연하고 대량의 데이터를 빠르게 처리할 수 있다는 장점을 가지고 있습니다. 하지만 관계형 데이터베이스와 달리 표준화된 쿼리 언어가 존재하지 않는 경우가 많으며, 데이터 일관성이 상대적으로 약할 수 있다는 특징도 가지고 있습니다.&lt;br /&gt;이러한 이유로 NoSQL 데이터베이스는 관계형 데이터베이스를 완전히 대체하기보다는, 특정 용도에 맞게 함께 사용하는 보조적인 데이터 저장소로 활용되는 경우가 많습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;새로운 데이터 베이스: New SQL&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NewSQL은 관계형 데이터베이스와 NoSQL 데이터베이스의 장점을 결합하려는 목적으로 등장한 데이터베이스 유형입니다. 기존 관계형 데이터베이스는 ACID 트랜잭션을 기반으로 강한 데이터 일관성을 보장하지만, 대규모 분산 환경에서 수평 확장이 어렵다는 단점이 있었습니다. 반면 NoSQL 데이터베이스는 높은 확장성과 성능을 제공하지만, 데이터 일관성이 약해질 수 있다는 특징이 있습니다.&lt;br /&gt;&lt;br /&gt;NewSQL은 이러한 문제를 해결하기 위해 등장했습니다. &lt;br /&gt;SQL과 ACID 트랜잭션을 유지하면서도 분산 환경에서 수평 확장이 가능하도록 설계된 데이터베이스라고 볼 수 있습니다.&lt;br /&gt;대표적인 NewSQL 데이터베이스로는 Google Spanner, CockroachDB, TiDB 등이 있습니다.&lt;br /&gt;정리하면, &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;NewSQL은 관계형 데이터베이스의 &lt;/span&gt;강한 일관성&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;과 NoSQL의 &lt;/span&gt;확장성&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;을 함께 제공하기 위해 등장한 데이터베이스입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 아직까지는 생태계와 인프라가 충분히 성숙하지 않았다는 단점이 있습니다. 이러한 이유로 현재까지는 관계형 데이터베이스와 NoSQL 데이터베이스를 함께 사용하는 방식이 더 널리 사용되고 있습니다.&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;br /&gt;여기서 말하는 시계열 데이터란 시간(Time)과 함께 기록되는 데이터를 의미합니다.&lt;br /&gt;예를 들어 다음과 같은 데이터를 생각해볼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772721075884&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;10:00 CPU 사용률 30%
10:01 CPU 사용률 35%
10:02 CPU 사용률 33%&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 시간의 흐름에 따라 계속 기록되는 데이터를 시계열 데이터라고 합니다.&lt;br /&gt;이러한 데이터는 일반적인 데이터베이스에서도 저장할 수 있지만, 데이터 양이 매우 많고 지속적으로 기록되기 때문에 효율적으로 저장하고 조회하는 데 한계가 있을 수 있습니다.&lt;br /&gt;그래서 등장한 것이 시계열 데이터베이스(Time-Series DB)입니다.&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-end=&quot;620&quot; data-start=&quot;520&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;537&quot; data-start=&quot;520&quot;&gt;매우 높은 데이터 쓰기 성능&lt;/li&gt;
&lt;li data-end=&quot;556&quot; data-start=&quot;538&quot;&gt;시간 기반 데이터 조회 최적화&lt;/li&gt;
&lt;li data-end=&quot;568&quot; data-start=&quot;557&quot;&gt;데이터 압축 기능&lt;/li&gt;
&lt;li data-end=&quot;620&quot; data-start=&quot;569&quot;&gt;일정 시간이 지나면 데이터를 자동으로 삭제하는 보존 정책(retention policy)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;666&quot; data-start=&quot;622&quot; data-ke-size=&quot;size16&quot;&gt;이러한 특징 때문에 시계열 데이터베이스는 다음과 같은 분야에서 많이 사용됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;720&quot; data-start=&quot;668&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;677&quot; data-start=&quot;668&quot;&gt;서버 모니터링&lt;/li&gt;
&lt;li data-end=&quot;690&quot; data-start=&quot;678&quot;&gt;IoT 센서 데이터&lt;/li&gt;
&lt;li data-end=&quot;702&quot; data-start=&quot;691&quot;&gt;금융 데이터 분석&lt;/li&gt;
&lt;li data-end=&quot;720&quot; data-start=&quot;703&quot;&gt;로그 및 메트릭 데이터 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;774&quot; data-start=&quot;722&quot; data-ke-size=&quot;size16&quot;&gt;대표적인 시계열 데이터베이스로는 InfluxDB, TimescaleDB 등이 있습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;774&quot; data-start=&quot;722&quot; data-ke-size=&quot;size23&quot;&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-end=&quot;467&quot; data-start=&quot;351&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;376&quot; data-start=&quot;351&quot;&gt;시간이 지날수록 데이터가 계속 추가됩니다.&lt;/li&gt;
&lt;li data-end=&quot;407&quot; data-start=&quot;377&quot;&gt;대부분 쓰기(Insert) 작업이 많습니다.&lt;/li&gt;
&lt;li data-end=&quot;435&quot; data-start=&quot;408&quot;&gt;기존 데이터를 수정하는 경우는 거의 없습니다.&lt;/li&gt;
&lt;li data-end=&quot;467&quot; data-start=&quot;436&quot;&gt;특정 시간 범위를 기준으로 조회하는 경우가 많습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;578&quot; data-start=&quot;469&quot; data-ke-size=&quot;size16&quot;&gt;하지만 일반적인 관계형 데이터베이스는 이러한 데이터 패턴에 최적화되어 있지 않습니다.&lt;br /&gt;데이터가 계속 쌓이게 되면 저장 공간이 빠르게 증가하고, 시간 기반 조회 성능도 점점 떨어질 수 있습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그래서 등장한 것이 시계열 데이터베이스(Time-Series DB)입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;578&quot; data-start=&quot;469&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 관계형 데이터베이스, NoSQL, NewSQL, 시계열 데이터베이스와 같은 다양한 데이터베이스 유형에 대해 간단하게 살펴보았습니다. 처음에는 새롭게 알게 된 NewSQL과 시계열 데이터베이스를 중심으로 정리해보려고 했지만, 생각보다 정리할 수 있는 내용이 많지 않았습니다. 반면 NoSQL은 여러 가지 유형(Document, Column, Key-Value, Graph)이 존재하다 보니 자연스럽게 NoSQL에 대한 분량이 더 많아졌습니다. 지금까지 ByteByteGo 글을 읽으면서 주로 아키텍처나 설계와 관련된 내용을 중심으로 학습해 왔지만, 장애 상황에 대한 글들은 아직 많이 읽어보지 못했습니다. 그래서 다음 주부터는 서비스 장애나 시스템 장애와 관련된 글들을 중심으로 학습해볼 계획입니다.&lt;br /&gt;&lt;br /&gt;하나의 데이터베이스가 모든 문제를 해결할 수는 없기 때문에, 서비스의 특성과 요구사항에 맞게 적절한 데이터베이스를 선택하는 것이 중요하다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;br /&gt;&lt;a href=&quot;https://blog.bytebytego.com/p/understanding-database-types&quot;&gt;https://blog.bytebytego.com/p/understanding-database-types&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/543</guid>
      <comments>https://b-programmer.tistory.com/543#entry543comment</comments>
      <pubDate>Thu, 5 Mar 2026 23:39:56 +0900</pubDate>
    </item>
    <item>
      <title>kafka가 Zookeeper대신 KRaft를 사용하는 이유가 뭘까?</title>
      <link>https://b-programmer.tistory.com/542</link>
      <description>&lt;figure id=&quot;og_1772617924251&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;카프카를 왜 사용해야 할까?&quot; data-og-description=&quot;카프카를 계속 공부해 왔지만, 솔직히 말하면 &amp;quot;왜 이걸 써야 하는지&amp;quot; 스스로 완전히 납득했다고 말하기는 어렵습니다.문서와 강의에서는 늘 대규모 트래픽, 이벤트 스트리밍, 비동기 아키텍처 &quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/510&quot; data-og-url=&quot;https://b-programmer.tistory.com/510&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/9jHX3/dJMb8QejUM3/oZCeAYq51X14elgCX3t3ok/img.png?width=800&amp;amp;height=560&amp;amp;face=0_0_800_560,https://scrap.kakaocdn.net/dn/LkmtG/dJMb8Z3oYqc/kKFuLK7XIsyqV8gFLcxUG0/img.png?width=800&amp;amp;height=560&amp;amp;face=0_0_800_560,https://scrap.kakaocdn.net/dn/vlWsl/dJMb8TB7iNt/RSH3uHxxF3VC82vX4dDrdK/img.png?width=1502&amp;amp;height=1052&amp;amp;face=0_0_1502_1052&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/510&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/510&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/9jHX3/dJMb8QejUM3/oZCeAYq51X14elgCX3t3ok/img.png?width=800&amp;amp;height=560&amp;amp;face=0_0_800_560,https://scrap.kakaocdn.net/dn/LkmtG/dJMb8Z3oYqc/kKFuLK7XIsyqV8gFLcxUG0/img.png?width=800&amp;amp;height=560&amp;amp;face=0_0_800_560,https://scrap.kakaocdn.net/dn/vlWsl/dJMb8TB7iNt/RSH3uHxxF3VC82vX4dDrdK/img.png?width=1502&amp;amp;height=1052&amp;amp;face=0_0_1502_1052');&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;카프카를 왜 사용해야 할까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카프카를 계속 공부해 왔지만, 솔직히 말하면 &quot;왜 이걸 써야 하는지&quot; 스스로 완전히 납득했다고 말하기는 어렵습니다.문서와 강의에서는 늘 대규모 트래픽, 이벤트 스트리밍, 비동기 아키텍처&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 카프카가 어떻게 동작하며 왜 필요한지에 대해 학습한 적이 있습니다. 간단히 복습해 보겠습니다. 카프카는 단순한 MQ 시스템이라고 보기는 어렵습니다. MQ 시스템의 구조와 유사한 점은 있지만, 보다 정확하게 표현하면 &lt;b&gt;중앙 이벤트 로깅 시스템&lt;/b&gt;에 가깝습니다. 물론 전체적인 구조는 MQ와 크게 다르지 않습니다. 프로듀서(Producer)가 메시지를 발행하고, 컨슈머(Consumer)가 이를 소비하며, 그 사이에는 메시지를 저장하고 전달하는 브로커(Broker)이 존재합니다. 그래서 처음 접하게 되면 &quot;그냥 여러 번 읽을 수 있는 MQ 시스템 아닌가?&quot;라고 이해할 수도 있습니다. 이러한 이해도 크게 틀린 것은 아니지만, 카프카를 단순히 MQ라고 표현하는 것은 다소 부족한 설명이 될 수 있습니다. 그렇다면 한 가지 의문이 생길 수 있습니다. 왜 프로듀서와 컨슈머만 있으면 되지, 굳이 브로커가 존재해야 할까요? 간단히 설명하면 브로커를 사용하는 이유는 효용성때문으로 이해하시면 됩니다. 자세한 내용은&lt;/p&gt;
&lt;figure id=&quot;og_1772686754436&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;EDA에도 패턴들이 이렇게나 많이 존재 하다니..&quot; data-og-description=&quot;EDA(Event Driven Architecture)가 MQ나 이벤트 리스너를 통해 동작한다는 것은 알고 있었습니다. 하지만 &amp;quot;어느 상황에서 사용해야 하는가?&amp;quot;에 대해서는 명확하게 정리되어 있지 않았습니다. 단순히 도메&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/533&quot; data-og-url=&quot;https://b-programmer.tistory.com/533&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ocQKw/dJMb81GUZfS/kKtvGxYSv2vab507oSaLN0/img.png?width=800&amp;amp;height=468&amp;amp;face=0_0_800_468,https://scrap.kakaocdn.net/dn/f8hyE/dJMb8RjZIdJ/GkWYzpjSyxB308LolGaSz0/img.png?width=800&amp;amp;height=468&amp;amp;face=0_0_800_468,https://scrap.kakaocdn.net/dn/BOm4T/dJMb85vMHsm/4KD1eZyBU6nBFmL0c9OVU0/img.png?width=1782&amp;amp;height=1044&amp;amp;face=0_0_1782_1044&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/533&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/533&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ocQKw/dJMb81GUZfS/kKtvGxYSv2vab507oSaLN0/img.png?width=800&amp;amp;height=468&amp;amp;face=0_0_800_468,https://scrap.kakaocdn.net/dn/f8hyE/dJMb8RjZIdJ/GkWYzpjSyxB308LolGaSz0/img.png?width=800&amp;amp;height=468&amp;amp;face=0_0_800_468,https://scrap.kakaocdn.net/dn/BOm4T/dJMb85vMHsm/4KD1eZyBU6nBFmL0c9OVU0/img.png?width=1782&amp;amp;height=1044&amp;amp;face=0_0_1782_1044');&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;EDA에도 패턴들이 이렇게나 많이 존재 하다니..&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;EDA(Event Driven Architecture)가 MQ나 이벤트 리스너를 통해 동작한다는 것은 알고 있었습니다. 하지만 &quot;어느 상황에서 사용해야 하는가?&quot;에 대해서는 명확하게 정리되어 있지 않았습니다. 단순히 도메&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 참고해주면 감사하겠습니다. 초반에는 브로커와 브로커의 메타 정보를 읽는 Zookeeper가 존재하였습니다. 그렇다면 어째서 kafka내부에서 동작시키지않고 Zookeeper라는 외부 인프라를 이용하게 되었을까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Zookeeper는 무엇이고, 왜 사용하게 되었을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KRaft를 이해하기 전에 먼저 Zookeeper라는 시스템을 이해할 필요가 있습니다.&lt;br /&gt;Zookeeper는 분산 시스템에서 상태를 관리하고 여러 노드의 동작을 조율하는 분산 코디네이션 시스템입니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;kafka 초기 버전에서는 여러 브로커가&amp;nbsp;함께 동작하는 클러스터 구조를 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dI5xGN/dJMcabDlouy/bfoAlLj9ntR5oS2jUvKoQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dI5xGN/dJMcabDlouy/bfoAlLj9ntR5oS2jUvKoQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dI5xGN/dJMcabDlouy/bfoAlLj9ntR5oS2jUvKoQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdI5xGN%2FdJMcabDlouy%2FbfoAlLj9ntR5oS2jUvKoQ0%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;336&quot; height=&quot;317&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;565&quot; data-start=&quot;431&quot; data-ke-size=&quot;size16&quot;&gt;이때 어떤 브로커가 살아있는지, 누가 리더인지, 토픽과 파티션의 메타데이터는 무엇인지와 같은 클러스터 상태 정보를 관리할 필요가 있습니다. 여기서 브로커가 왜 이렇게까지 많이 필요할 이유가 있을까요? 브로커가 많다는 건 어떤 이점이 있을까요?&lt;/p&gt;
&lt;p data-end=&quot;565&quot; data-start=&quot;431&quot; data-ke-size=&quot;size16&quot;&gt;kafka는 기본적으로 대량의 데이터를 처리하기 위해 설계된 시스템입니다.&lt;br /&gt;단일 서버로는 처리할 수 없는 수준의 트래픽과 데이터를 안정적으로 처리하기 위해 여러 개의 브로커를 사용하는 클러스터 구조를 채택하고 있습니다. (레디스의 전략과는 사뭇 다른 느낌이 듭니다. _.._)&lt;/p&gt;
&lt;h3 data-end=&quot;565&quot; data-start=&quot;431&quot; data-ke-size=&quot;size23&quot;&gt;어째서 위와 같은 클러스터 상태&amp;nbsp; 정보들을 관리하는 이유가 무엇일까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;kafka 초기 버전에서는 클러스터 상태 정보들을 관리하는 역할을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Zookeeper가 담당하고 있었습니다.&lt;br /&gt;kafka는 여러 개의 브로커로 구성된 클러스터 환경에서 동작합니다.&lt;br /&gt;이러한 구조에서는 단순히 메시지를 저장하는 것뿐만 아니라, 클러스터 전체의 상태를 지속적으로 관리해야 합니다.&lt;br /&gt;이처럼 분산 환경에서는 시스템의 상태가 계속해서 변화할 수 있습니다.&lt;br /&gt;kafka는 단순히 메시지를 전달하는 시스템을 넘어, 여러 서버가 함께 동작하는 분산 시스템이기 때문에 이러한 클러스터 상태 정보들을 관리하는 역할이 반드시 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 분산 환경에서 클러스터 상태 정보를 관리하기 위해 Zookeeper라는 분산 코디네이션 시스템을 사용하게 되었습니다.&lt;/p&gt;
&lt;p 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;다른 기술들도 많을텐데 Zookeeper를 사용하는 이유가 무엇일까?&lt;/h3&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;u&gt;분산 시스템의 상태를 관리하는 표준적인 도구로 Zookeeper가 널리 사용&lt;/u&gt;되고 있었습니다.&lt;br /&gt;Zookeeper는 다음과 같은 기능들을 제공하고 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;619&quot; data-end=&quot;732&quot;&gt;
&lt;li data-start=&quot;619&quot; data-end=&quot;656&quot;&gt;분산 환경에서의&lt;span&gt;&amp;nbsp;&lt;/span&gt;리더 선출(Leader Election)&lt;/li&gt;
&lt;li data-start=&quot;657&quot; data-end=&quot;685&quot;&gt;&lt;u&gt;분산 락(Distributed Lock)&lt;/u&gt;&lt;/li&gt;
&lt;li data-start=&quot;686&quot; data-end=&quot;705&quot;&gt;클러스터 메타데이터 관리&lt;/li&gt;
&lt;li data-start=&quot;706&quot; data-end=&quot;732&quot;&gt;노드 상태를 감지하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Watch 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;734&quot; data-end=&quot;871&quot; data-ke-size=&quot;size16&quot;&gt;이러한 기능들은 kafka와 같은 분산 시스템에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;클러스터 상태를 관리하기 위해 필요한 요소들이었습니다.&lt;br /&gt;따라서 &lt;u&gt;kafka는 이러한 기능을 직접 구현하기보다는 이미 검증된&lt;span&gt;&amp;nbsp;&lt;/span&gt;Zookeeper를 활용하는 방식을 선택&lt;/u&gt;하게 되었습니다.&lt;br /&gt;결과적으로 kafka는 초기 설계에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;클러스터 상태 관리 역할을 Zookeeper에 위임하는 구조로 만들어지게 되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;초창기 kafka에서 Zookeeper를 사용하지 않는 다면, 어떻게 될까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafka는 여러 브로커로 구성된 클러스터 환경에서 동작합니다.&lt;br /&gt;이 환경에서는 브로커의 상태, 파티션의 리더 정보, 토픽 메타데이터와 같은 클러스터 상태 정보를 지속적으로 관리해야 합니다.&lt;br /&gt;만약 이러한 정보들을 관리하는 시스템이 존재하지 않는다면 다음과 같은 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;385&quot; data-start=&quot;301&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;325&quot; data-start=&quot;301&quot;&gt;어떤 브로커가 살아있는지 알 수 없음&lt;/li&gt;
&lt;li data-end=&quot;352&quot; data-start=&quot;326&quot;&gt;파티션의 리더가 누구인지 결정할 수 없음&lt;/li&gt;
&lt;li data-end=&quot;385&quot; data-start=&quot;353&quot;&gt;토픽 및 파티션 메타데이터가 일관되게 유지되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;460&quot; data-start=&quot;387&quot; data-ke-size=&quot;size16&quot;&gt;이러한 상황에서는 클러스터가 일관된 상태를 유지하기 어렵기 때문에 kafka 시스템 자체가 정상적으로 동작하기 어려워집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zookeeper는 Kafka와는 별개의 시스템입니다. 그렇다면 Zookeeper에서 장애가 발생하면 어떻게 되는 것일까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하자면, Zookeeper 역시 단일 서버로 운영되지 않습니다. kafka와 마찬가지로 클러스터 형태로 구성하여 장애에 대응합니다.&lt;br /&gt;Zookeeper는 여러 개의 노드로 구성된 Zookeeper Ensemble이라는 구조를 사용합니다.&lt;br /&gt;일반적으로 3개 ~ 5개로 사용하는 경우가 많습니다. 이러한 구조에서는 과반수(Quorum) 기반으로 시스템이 동작합니다.&lt;br /&gt;즉, 전체 노드 중 절반 이상이 정상적으로 동작하고 있다면 시스템은 계속해서 동작할 수 있습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 Zookeeper 노드가 3개라고 가정해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bihBkf/dJMcagYVQQJ/6qr5zh3ieVjtaTxP71lnk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bihBkf/dJMcagYVQQJ/6qr5zh3ieVjtaTxP71lnk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bihBkf/dJMcagYVQQJ/6qr5zh3ieVjtaTxP71lnk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbihBkf%2FdJMcagYVQQJ%2F6qr5zh3ieVjtaTxP71lnk1%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;359&quot; height=&quot;195&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;607&quot; data-start=&quot;537&quot; data-ke-size=&quot;size16&quot;&gt;이 중 한 노드가 장애가 발생하더라도 나머지 두 노드가 살아 있다면 과반수를 유지하기 때문에 시스템은 계속 동작합니다.&lt;br /&gt;하지만 만약 &lt;u&gt;과반수 이상의 노드가 장애가 발생하게 된다면 클러스터의 합의가 깨지게 되고 정상적인 동작이 어려워질 수있습니다.&lt;/u&gt;&lt;br /&gt;이러한 이유로 Zookeeper는 &lt;u&gt;단일 노드로 운영하기보다는 여러 노드로 구성된 클러스터 형태로 운영하는 것이 일반적입니다.&lt;/u&gt;&lt;br /&gt;결과적으로 kafka는 브로커 클러스터 외에도 Zookeeper 클러스터를 함께 운영해야 하는 구조를 가지고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합의는 분산락 3대 요소중 하나로 다음 포스트를 참고하시면 됩니다.&lt;/p&gt;
&lt;figure id=&quot;og_1772627274375&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;분산락은 도대체 무엇일까?&quot; data-og-description=&quot;이전에도 Lock에 대해 학습을 진행한적이 있었습니다. DB락 부터, 낙관 락, 비관 락에 대해 학습을 진행하였죠. RDB vs Nosql크게 DB에는 2가지 종류가 존재합니다. SQL을 사용하는 RDB와 SQL도 사용하는 N&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/531&quot; data-og-url=&quot;https://b-programmer.tistory.com/531&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/N6zse/dJMb8YXJc5g/tzG7dyIMKmH5SoVtdZOVr1/img.png?width=534&amp;amp;height=282&amp;amp;face=0_0_534_282,https://scrap.kakaocdn.net/dn/csYQjh/dJMb8U8RlZz/GODk7jTZJBukqfKyI5vx31/img.png?width=534&amp;amp;height=282&amp;amp;face=0_0_534_282&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/531&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/531&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/N6zse/dJMb8YXJc5g/tzG7dyIMKmH5SoVtdZOVr1/img.png?width=534&amp;amp;height=282&amp;amp;face=0_0_534_282,https://scrap.kakaocdn.net/dn/csYQjh/dJMb8U8RlZz/GODk7jTZJBukqfKyI5vx31/img.png?width=534&amp;amp;height=282&amp;amp;face=0_0_534_282');&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;분산락은 도대체 무엇일까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이전에도 Lock에 대해 학습을 진행한적이 있었습니다. DB락 부터, 낙관 락, 비관 락에 대해 학습을 진행하였죠. RDB vs Nosql크게 DB에는 2가지 종류가 존재합니다. SQL을 사용하는 RDB와 SQL도 사용하는 N&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.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;p data-ke-size=&quot;size16&quot;&gt;분산 환경에서 클러스터의 상태를 관리하기 위해 초창기 kafka는 Zookeeper를 사용했습니다. 하지만 kafka를 운영하기 위해서는 kafka 브로커뿐만 아니라 Zookeeper 서버도 함께 운영해야 하는 상황이 발생했습니다. 또한 Zookeeper에 장애가 발생할 경우, 심각한 상황에서는 kafka 클러스터의 관리 기능이 정상적으로 동작하지 않을 수도 있습니다. 이처럼 별도의 시스템에 의존하는 구조는 운영 복잡도를 높이는 문제가 있었습니다. 이러한 문제를 해결하기 위해 &lt;u&gt;kafka는 Zookeeper 없이 메타데이터를 관리할 수 있는 구조인 KRaft(Kafka Raft Metadata mode) 를 도입&lt;/u&gt;하게 되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;KRaft는 Zookeeper과 무엇이 다른가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Zookeeper가 어떤 역할을 하는지 대략적으로 살펴보았습니다. 그리고 이러한 역할을 이제는 KRaft가 대신 수행하게 되었습니다. 이전의 kafka 구조에서는 브로커만으로 클러스터를 운영할 수 없었기 때문에 클러스터 상태 관리, 리더 선출, 메타데이터 관리와 같은 기능들을 Zookeeper가 담당하고 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;374&quot; data-start=&quot;305&quot; data-ke-size=&quot;size16&quot;&gt;즉, kafka가 직접 처리하지 못하는 클러스터 관리 기능을 외부 시스템인 Zookeeper에 의존하는 구조였습니다.&lt;/p&gt;
&lt;p data-end=&quot;514&quot; data-start=&quot;376&quot; data-ke-size=&quot;size16&quot;&gt;하지만 KRaft가 도입되면서 이러한 구조가 변경되었습니다. 이제 kafka는 Zookeeper 없이도 클러스터 메타데이터를 관리할 수 있게 되었으며, 관련 기능들은 kafka 내부에 포함된 KRaft 모듈이 담당하게 됩니다.&lt;br /&gt;결과적으로 kafka는 더 이상 외부 시스템에 의존하지 않고 kafka 내부에서 클러스터 상태를 관리할 수 있는 구조로 변화하게 되었습니다.&lt;br /&gt;이러한 구조 덕분에 기존처럼 Zookeeper 장애로 인해 kafka 운영이 복잡해지는 상황을 줄일 수 있게 되었습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;514&quot; data-start=&quot;376&quot; data-ke-size=&quot;size23&quot;&gt;그렇다면 이러한 구조적인 차이 외에도 KRaft는 Zookeeper와 어떤 차이점이 있을까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 차이점은 메타데이터를 관리하는 방식입니다.&lt;br /&gt;기존 kafka 구조에서는 브로커의 상태, 토픽 정보, 파티션 정보와 같은 클러스터 메타데이터를 Zookeeper에 저장하고 관리했습니다.&lt;br /&gt;kafka 브로커들은 필요할 때마다 Zookeeper와 통신하여 해당 정보를 조회하거나 변경했습니다.&lt;br /&gt;하지만 KRaft 구조에서는 이러한 &lt;u&gt;메타데이터를 kafka 내부의 로그(Log) 형태로 저장하고 관리&lt;/u&gt;합니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;br /&gt;즉, kafka가 메시지를 로그 형태로 저장하는 것처럼 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;클러스터 메타데이터 역시 &lt;/span&gt;&lt;u&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;로그 기반으로 기록하고 복제하는 방식&lt;/span&gt;&lt;/u&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;u&gt;을 사용&lt;/u&gt;합니다.&lt;br /&gt;&lt;/span&gt;이 과정에서 Raft 합의 알고리즘이 사용됩니다. Raft 합의 알고리즘을 통해 여러 kafka 노드가 메타데이터에 대해 합의를 이루고, 이를 통해 일관된 상태를 유지할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Raft 합의 알고리즘이란 무엇일까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Raft는 분산 시스템에서 여러 노드가 동일한 상태를 유지하기 위해 사용하는 &lt;u&gt;합의(Consensus) 알고리즘&lt;/u&gt;입니다.&lt;br /&gt;분산 시스템에서는 여러 서버가 동시에 동작하기 때문에 다음과 같은 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;322&quot; data-start=&quot;227&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;256&quot; data-start=&quot;227&quot;&gt;어떤 노드의 정보가 최신 상태인지 알기 어려움&lt;/li&gt;
&lt;li data-end=&quot;288&quot; data-start=&quot;257&quot;&gt;동시에 여러 노드에서 상태 변경이 발생할 수 있음&lt;/li&gt;
&lt;li data-end=&quot;322&quot; data-start=&quot;289&quot;&gt;네트워크 장애로 인해 노드 간 상태가 달라질 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;444&quot; data-start=&quot;324&quot; data-ke-size=&quot;size16&quot;&gt;이러한 상황에서 시스템이 정상적으로 동작하기 위해서는 모든 노드(브로커)가 동일한 상태에 대해 합의하는 과정이 필요합니다.&lt;br /&gt;Raft 알고리즘은 바로 이러한 문제를 해결하기 위해 만들어진 합의 알고리즘입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;515&quot; data-start=&quot;446&quot;&gt;여러 노드(브로커) 중 하나를 Leader로 선출하고, 나머지 노드(브로커)들은 Follower로 동작합니다.&lt;/li&gt;
&lt;li data-end=&quot;515&quot; data-start=&quot;446&quot;&gt;Leader는 시스템의 &lt;u&gt;상태 변경을 관리하는 역할&lt;/u&gt;을 하며, &lt;u&gt;상태 변경이 발생하면 이를 로그(Log) 형태로 기록한 뒤 다른 노드들에게 전파&lt;/u&gt;합니다.&lt;/li&gt;
&lt;li data-end=&quot;515&quot; data-start=&quot;446&quot;&gt;과반수 이상의 노드(브로커)가 해당 로그를 받아들이면 그 상태 변경은 합의된 상태(Committed) 로 인정됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;515&quot; data-start=&quot;446&quot; data-ke-size=&quot;size16&quot;&gt;이러한 과정을 통해 여러 서버가 존재하더라도 모든 노드(브로커)가 동일한 상태를 유지할 수 있게 됩니다.&lt;br /&gt;kafka의 KRaft 구조에서도 이 Raft 알고리즘을 사용하여 클러스터 메타데이터를 관리하고 노드(브로커) 간 상태를 일관되게 유지합니다.&lt;/p&gt;
&lt;h4 data-end=&quot;736&quot; data-start=&quot;680&quot; data-ke-size=&quot;size20&quot;&gt;그렇담, Zookeeper는 어떤 합의 알고리즘을 무엇을 사용할까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zookeeper 클러스터에서는 다음과 같은 구조가 만들어집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;437&quot; data-start=&quot;410&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;422&quot; data-start=&quot;410&quot;&gt;Leader&lt;/li&gt;
&lt;li data-end=&quot;437&quot; data-start=&quot;423&quot;&gt;Follower&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;494&quot; data-start=&quot;439&quot; data-ke-size=&quot;size16&quot;&gt;Leader가 상태 변경을 관리하고, Follower들은 Leader의 로그를 받아 복제합니다.&lt;br /&gt;동작 과정은 대략 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;644&quot; data-start=&quot;517&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;545&quot; data-start=&quot;517&quot;&gt;클라이언트 요청이 Leader에게 전달됩니다.&lt;/li&gt;
&lt;li data-end=&quot;583&quot; data-start=&quot;546&quot;&gt;Leader는 상태 변경을 트랜잭션 로그로 기록합니다.&lt;/li&gt;
&lt;li data-end=&quot;612&quot; data-start=&quot;584&quot;&gt;해당 로그를 Follower들에게 전파합니다.&lt;/li&gt;
&lt;li data-end=&quot;644&quot; data-start=&quot;613&quot;&gt;과반수 노드가 승인하면 commit 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;698&quot; data-start=&quot;646&quot; data-ke-size=&quot;size16&quot;&gt;이 방식 덕분에 여러 노드가 존재하더라도 데이터의 순서와 일관성을 유지할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-end=&quot;565&quot; data-start=&quot;431&quot; data-ke-size=&quot;size16&quot;&gt;kafka가 Zookeeper를 더 이상 사용하지 않고 KRaft를 사용하는 이유에 대해 살펴보았습니다. 기존 kafka 구조에서는 kafka 브로커와 함께 Zookeeper 서버를 별도로 운영해야 하는 구조였습니다. 이러한 구조는 시스템을 운영하는 과정에서 운영 복잡도를 증가시키는 요소가 되었습니다. 또한 Zookeeper에 심각한 장애가 발생할 경우, kafka 클러스터의 메타데이터 관리 기능에도 영향을 줄 수 있는 상황이 발생할 수 있습니다. 이러한 문제를 해결하기 위해 Kafka는 Zookeeper에 의존하지 않고 내부적으로 메타데이터를 관리하는 구조인 KRaft(Kafka Raft Metadata mode) 를 도입하게 되었습니다. KRaft는 Raft 합의 알고리즘을 기반으로 메타데이터를 관리하며, 이를 통해 분산 환경에서도 여러 노드가 동일한 상태를 유지하면서 안정적으로 시스템을 운영할 수 있도록 합니다. 결과적으로 kafka는 KRaft를 통해 시스템 구조를 단순화하고 운영 복잡도를 줄이는 방향으로 발전하게 되었습니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/542</guid>
      <comments>https://b-programmer.tistory.com/542#entry542comment</comments>
      <pubDate>Wed, 4 Mar 2026 23:06:34 +0900</pubDate>
    </item>
    <item>
      <title>AI시대에서 클린 아키텍처 이해하기</title>
      <link>https://b-programmer.tistory.com/541</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 우리는 AI 시대에 살고 있습니다. &quot;AI가 코드를 다 작성해주는데, 이제 아키텍처를 공부하는 것이 과연 의미가 있을까?&quot;&lt;br /&gt;이런 질문을 종종 듣게 됩니다. 어느 정도는 공감이 가는 말입니다. 실제로 AI는 빠르게 코드를 생성하고, 기본적인 구조를 만들어내며, 심지어 리팩토링까지 제안합니다. 과거에 비해 구현의 진입 장벽은 확실히 낮아졌습니다. 하지만 한 가지는 분명합니다. AI가 작성한 코드의 책임은 결국 사람이 져야 한다는 점입니다. AI는 코드를 생성할 수 있지만, 그 코드가 시스템의 맥락에 맞는지, 장기적으로 유지 가능한지, 확장 시 문제가 발생하지는 않는지까지 완전히 보장해주지는 않습니다. 설령 AI를 신뢰한다고 하더라도, 언제나 동일한 환경과 도구를 사용할 수 있다는 보장도 없습니다. 만약 AI의 도움 없이 코드를 작성해야 하는 상황이 온다면, 결국 설계와 구조에 대한 이해는 사람의 몫이 됩니다. 그래서 저는 이번에 클린 아키텍처를 학습해보려 합니다. AI가 코드를 대신 작성해주는 시대일수록, 오히려 시스템의 중심이 되는 구조와 책임에 대해 더 깊이 고민해야 하지 않을까 하는 생각에서입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클린 아키텍처란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 아키텍처는 소프트웨어 시스템을 여러 계층으로 분리하여 각 계층이 명확한 책임을 가지도록 설계하는 구조입니다. 그렇다면 왜 여러 계층으로 나누는 것이 &quot;클린하다&quot;고 표현하는 걸까요? 클린하다는 것은 계층이 많아서 깨끗하다는 뜻이 아닙니다.&lt;br /&gt;오히려 계층이 늘어나면 구조는 더 복잡해질 수도 있습니다. 그럼에도 불구하고 클린이라고 부르는 이유는 따로 있습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;322&quot; data-start=&quot;297&quot; data-ke-size=&quot;size23&quot;&gt;소프트웨어가 &quot;더러워지는&quot; 순간은 언제일까요?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;434&quot; data-start=&quot;324&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;348&quot; data-start=&quot;324&quot;&gt;컨트롤러에 비즈니스 로직이 섞여 있고,&lt;/li&gt;
&lt;li data-end=&quot;381&quot; data-start=&quot;349&quot;&gt;도메인 객체가 DB 어노테이션에 강하게 묶여 있으며,&lt;/li&gt;
&lt;li data-end=&quot;408&quot; data-start=&quot;382&quot;&gt;서비스가 HTTP 응답 포맷을 알고 있고,&lt;/li&gt;
&lt;li data-end=&quot;434&quot; data-start=&quot;409&quot;&gt;하나의 클래스가 여러 이유로 변경되는 상황입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;481&quot; data-start=&quot;436&quot; data-ke-size=&quot;size16&quot;&gt;이때 코드가 복잡해지는 이유는 하나입니다. 기술과 책임이 뒤섞였기 때문입니다.&lt;br /&gt;클린 아키텍처는 이 혼합을 강제로 분리합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;553&quot; data-start=&quot;510&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;525&quot; data-start=&quot;510&quot;&gt;이 코드는 비즈니스인가?&lt;/li&gt;
&lt;li data-end=&quot;536&quot; data-start=&quot;526&quot;&gt;기술 구현인가?&lt;/li&gt;
&lt;li data-end=&quot;553&quot; data-start=&quot;537&quot;&gt;외부 시스템과의 연결인가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;630&quot; data-start=&quot;555&quot; data-ke-size=&quot;size16&quot;&gt;이 질문을 끊임없이 던지게 만듭니다. 책임이 명확해질수록 코드의 의도는 선명해집니다. 그 상태를 우리는 &quot;클린하다&quot;고 부릅니다.&lt;br /&gt;또한, 클린 아키텍처는 사실 &quot;시간&quot;을 고려한 설계입니다. &lt;br /&gt;시간이 지나면 프레임워크는 바뀌고, DB는 교체되며, API 방식은 변하고, 팀 구성도 달라집니다. &lt;br /&gt;하지만 비즈니스 규칙은 비교적 오래 유지됩니다. 그래서 클린 아키텍처는 변하지 않는 것을 중심에 두고, 쉽게 변하는 것을 바깥으로 밀어냅니다. 의존성의 방향을 통제하여 변화가 중심을 오염시키지 않도록 합니다. 시간이 지나도 핵심이 유지되는 구조. 그것이 바로 깨끗한 상태를 유지하는 구조입니다. 즉, 클린하다는 것은 책임이 명확하고, 의존성이 통제되어 있으며, 변화가 중심을 침범하지 못하는 상태를 의미합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EJIzX/dJMcabceC6P/noJg9312mrb0IjlhNRG090/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EJIzX/dJMcabceC6P/noJg9312mrb0IjlhNRG090/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EJIzX/dJMcabceC6P/noJg9312mrb0IjlhNRG090/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEJIzX%2FdJMcabceC6P%2FnoJg9312mrb0IjlhNRG090%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;436&quot; height=&quot;515&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;860&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 원칙 (Key Principles)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 아키텍처는 유지보수성과 확장성, 테스트 용이성을 확보하기 위한 몇 가지 핵심 원칙을 기반으로 합니다.&lt;br /&gt;이 원칙들은 시스템의 책임을 명확히 하고, 변화에 강한 구조를 만들기 위한 기준이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관심사 분리 (Separation of Concerns)&lt;/h3&gt;
&lt;p data-end=&quot;205&quot; data-start=&quot;110&quot; data-ke-size=&quot;size16&quot;&gt;관심사 분리는 각 계층이 자신의 책임만 가지도록 명확히 분리하는 원칙입니다.&lt;br /&gt;하나의 구성 요소가 여러 역할을 동시에 수행하지 않도록 하여 책임의 경계를 분명히 합니다.&lt;/p&gt;
&lt;p data-end=&quot;324&quot; data-start=&quot;207&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 컨트롤러는 요청을 받고 응답을 반환하는 역할만 수행해야 합니다. &lt;br /&gt;실제 비즈니스 규칙은 서비스나 유스케이스 계층에서 처리하며, 데이터 저장과 같은 기술적 세부사항은 리포지토리 계층에서 담당합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pd0IF/dJMcaa5uh8N/v6KRdpx1BLlZaCGVWgEcPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pd0IF/dJMcaa5uh8N/v6KRdpx1BLlZaCGVWgEcPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pd0IF/dJMcaa5uh8N/v6KRdpx1BLlZaCGVWgEcPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpd0IF%2FdJMcaa5uh8N%2Fv6KRdpx1BLlZaCGVWgEcPk%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;541&quot; height=&quot;247&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;674&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;353&quot; data-start=&quot;326&quot; data-ke-size=&quot;size16&quot;&gt;이 접근 방식에는 두 가지 주요 이점이 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;441&quot; data-start=&quot;355&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫째&lt;/b&gt;, 각 계층이 단일 책임을 가지므로 코드의 의도가 명확해지고 유지보수가 쉬워집니다.&lt;br /&gt;변경이 필요할 때 어디를 수정해야 하는지 명확해지기 때문입니다.&lt;br /&gt;&lt;b&gt;둘째&lt;/b&gt;, 특정 계층의 변경이 다른 계층에 영향을 주지 않습니다.&lt;br /&gt;예를 들어 SQL 데이터베이스를 NoSQL로 전환하더라도, 데이터 접근 계층만 수정하면 되며 핵심 비즈니스 로직은 그대로 유지될 수 있습니다.&lt;br /&gt;관심사를 분리한다는 것은 단순히 코드를 나누는 것이 아니라, 변화의 범위를 통제하는 설계 전략입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 규칙 (Dependency Rule)&lt;/h3&gt;
&lt;p data-end=&quot;157&quot; data-start=&quot;80&quot; data-ke-size=&quot;size16&quot;&gt;의존성 규칙은 클린 아키텍처의 핵심 원칙입니다.&lt;br /&gt;이 규칙에 따르면 모든 의존성은 시스템의 중심을 향해, 즉 안쪽으로만 흘러가야 합니다.&lt;/p&gt;
&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;938&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brFukh/dJMcadVoFWh/SUTdxgYEfuXTUXEy4S9WsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brFukh/dJMcadVoFWh/SUTdxgYEfuXTUXEy4S9WsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brFukh/dJMcadVoFWh/SUTdxgYEfuXTUXEy4S9WsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrFukh%2FdJMcadVoFWh%2FSUTdxgYEfuXTUXEy4S9WsK%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;450&quot; height=&quot;354&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;938&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;282&quot; data-start=&quot;159&quot; data-ke-size=&quot;size16&quot;&gt;프레임워크, UI, 데이터베이스와 같은 외부 계층은 내부 계층(엔티티, 유스케이스)에 의존할 수 있지만, 내부 계층은 외부 계층을 알지 못해야 합니다. 핵심 비즈니스 로직은 기술적 세부사항에 의해 오염되어서는 안 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;445&quot; data-start=&quot;284&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 사용 사례는 특정 데이터베이스 스키마나 UI 프레임워크에 직접 의존해서는 안 됩니다. 대신 데이터 접근 계층은 리포지토리와 같은 인터페이스를 통해 계약만 제공하고, 실제 구현은 외부 계층에서 담당합니다. 데이터베이스가 변경되더라도 유스케이스와 엔티티는 수정할 필요가 없습니다.&lt;/p&gt;
&lt;p data-end=&quot;463&quot; data-start=&quot;447&quot; data-ke-size=&quot;size16&quot;&gt;이 규칙의 이점은 명확합니다.&lt;/p&gt;
&lt;p data-end=&quot;546&quot; data-start=&quot;465&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫째&lt;/b&gt;, &lt;u&gt;데이터베이스 교체와 같은 외부 변화가 핵심 비즈니스 로직에 영향을 주지 않습니다.&lt;/u&gt; 기술은 바뀌더라도 비즈니스 규칙은 안정적으로 유지됩니다.&lt;br /&gt;&lt;b&gt;둘째&lt;/b&gt;, 내부 레이어는 외부 시스템과 결합되지 않기 때문에 독립적으로 테스트할 수 있습니다. 실제 데이터베이스 없이도 핵심 로직을 검증할 수 있습니다.&lt;br /&gt;&lt;b&gt;셋째&lt;/b&gt;, 사용자 인터페이스나 데이터베이스와 같은 구성 요소를 최소한의 영향으로 교체할 수 있습니다. 예를 들어 REST API를 GraphQL로 전환하더라도 인터페이스 어댑터 계층만 수정하면 됩니다.&lt;br /&gt;&lt;b&gt;넷째&lt;/b&gt;, 새로운 기술이나 기능을 도입하더라도 핵심 논리를 건드리지 않고 시스템을 확장할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;928&quot; data-start=&quot;802&quot; data-ke-size=&quot;size16&quot;&gt;외부 시스템은 본질적으로 변동성이 큽니다. 웹 프레임워크는 업데이트되고, 라이브러리는 deprecated되며, 인프라는 교체됩니다. 의존성 규칙은 이러한 변화를 코어에서 격리함으로써 장기적으로 안정적인 구조를 유지하게 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단일 책임 원칙 (SRP)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 책임 원칙은 클래스나 모듈이 오직 하나의 이유로만 변경되어야 한다는 원칙입니다.&lt;br /&gt;즉, 하나의 구성 요소는 하나의 책임에만 집중해야 합니다. 여기서 말하는 &quot;책임&quot;은 단순한 기능 단위가 아니라, 변경의 이유를 의미합니다.&lt;br /&gt;만약 하나의 클래스가 서로 다른 이유로 수정될 가능성이 있다면, 이미 여러 책임을 동시에 가지고 있는 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;456&quot; data-start=&quot;298&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 하나의 클래스가 사용자 인증 로직을 처리하면서 동시에 이메일 알림까지 담당하고 있다면 이는 좋은 설계가 아닙니다. 인증 정책이 변경될 때도 수정되어야 하고, 이메일 발송 방식이 바뀌어도 수정되어야 하기 때문입니다. 서로 다른 변화가 하나의 클래스에 영향을 미치게 됩니다. 이 경우 인증을 담당하는 클래스와 알림을 담당하는 클래스를 분리하는 것이 바람직합니다. 각 클래스는 자신의 책임에만 집중하고, 서로 다른 변경 요인이 서로에게 영향을 주지 않도록 해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;679&quot; data-start=&quot;567&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;690&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/miHuR/dJMcag5Idlq/qkiGeVkCGBZNWWujXxll9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/miHuR/dJMcag5Idlq/qkiGeVkCGBZNWWujXxll9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/miHuR/dJMcag5Idlq/qkiGeVkCGBZNWWujXxll9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmiHuR%2FdJMcag5Idlq%2FqkiGeVkCGBZNWWujXxll9k%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;529&quot; height=&quot;348&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 레이어들은 동심원을 형성하며, 코어는 시스템의 가장 근본적이고 변하지 않는 부분을 포함하고 있으며, 외부 레이어는 사용자 인터페이스와 데이터베이스와 같은 기술별 세부 사항을 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 레이어를 더 자세히 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;계층 구조 (Layered Structure)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Entities (핵심 비즈니스 규칙)&lt;/h3&gt;
&lt;p data-end=&quot;129&quot; data-start=&quot;53&quot; data-ke-size=&quot;size16&quot;&gt;한국에서는 &amp;ldquo;도메인&amp;rdquo;이라는 용어가 더 익숙하게 사용됩니다. 실제로 많은 프로젝트에서 엔티티 계층을 도메인 계층이라고 부르기도 합니다.&lt;/p&gt;
&lt;p data-end=&quot;376&quot; data-start=&quot;229&quot; data-ke-size=&quot;size16&quot;&gt;다만 엄밀히 구분하자면, 클린 아키텍처에서 말하는 Entities는 단순한 도메인 모델을 넘어서는 개념입니다. 도메인은 문제 영역 전체를 의미하는 포괄적인 개념인 반면, 엔티티는 그 안에서 변하지 않는 핵심 비즈니스 규칙을 직접적으로 담고 있는 객체를 가리킵니다.&lt;/p&gt;
&lt;p data-end=&quot;444&quot; data-start=&quot;378&quot; data-ke-size=&quot;size16&quot;&gt;즉, 도메인이 &amp;ldquo;문제 영역 전체&amp;rdquo;라면, 엔티티는 그 영역 안에서 가장 본질적인 규칙을 표현하는 중심 모델에 가깝습니다.&lt;/p&gt;
&lt;p data-end=&quot;523&quot; data-start=&quot;446&quot; data-ke-size=&quot;size16&quot;&gt;그래서 용어는 익숙한 &amp;ldquo;도메인&amp;rdquo;을 사용할 수 있지만, 클린 아키텍처의 맥락에서는 &amp;ldquo;엔티티&amp;rdquo;라는 표현이 조금 더 정확하다고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;129&quot; data-start=&quot;53&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 코드가 있다고 가정해보겠습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
@Entity
@Table(name = &quot;boards&quot;)
public class Post extends BaseEntity {
...
public void reWrite(String title, String content) {
    ...
  }

  public void checkAuthor(String author) {
    ...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-end=&quot;129&quot; data-start=&quot;53&quot; data-ke-size=&quot;size16&quot;&gt;이 코드는 게시판 엔티티를 표현하고 있습니다.&lt;br /&gt;재작성(reWrite)과 작성자 확인(checkAuthor)이라는 행위를 포함하고 있는 점은 긍정적으로 볼 수 있습니다. 데이터만 담고 있는 단순한 객체가 아니라, 게시글이라는 개념이 가져야 할 행위를 함께 표현하고 있기 때문입니다. 그러나 한 가지 생각해볼 부분이 있습니다.&lt;br /&gt;이 클래스는 @Entity, @Table과 같은 JPA 어노테이션에 의존하고 있습니다. 즉, 이 객체는 이미 특정 ORM 프레임워크에 결합되어 있습니다. 클린 아키텍처 관점에서 보면, 핵심 엔티티는 프레임워크와 무관해야 합니다. (하지만 편의상 넣는 경우도 있습니다.)&lt;/p&gt;
&lt;p data-end=&quot;129&quot; data-start=&quot;53&quot; data-ke-size=&quot;size16&quot;&gt;데이터베이스가 JPA인지, MyBatis인지, 심지어 NoSQL인지조차 알 필요가 없습니다.&lt;br /&gt;이처럼 기술적 어노테이션이 직접 붙어 있는 순간,&lt;br /&gt;이 객체는 &quot;순수한 엔티티&quot;라기보다는 &quot;영속성 모델에 가까운 객체&quot;가 됩니다.&lt;br /&gt;결국 질문은 이것입니다. 이 객체는 비즈니스 규칙을 표현하는 중심 모델인가? 아니면 데이터 베이스 매핑을 위한 모델인가?&lt;br /&gt;클린 아키텍처에서 말하는 엔티티는 전자에 더 가깝습니다. 기술이 아닌, 비즈니스 자체를 표현하는 모델이어야 하기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;129&quot; data-start=&quot;53&quot; data-ke-size=&quot;size16&quot;&gt;정확히 클린아키텍처에서 말하는 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772547587963&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Post extends BaseEntity {
...
public void reWrite(String title, String content) {
    ...
  }

  public void checkAuthor(String author) {
    ...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Use Cases (애플리케이션 비즈니스 규칙)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 경우, 클린 아키텍처에서 말하는 Use Case는 우리가 흔히 사용하는 Service와 매우 비슷하게 느껴집니다. &lt;br /&gt;저 역시 처음에는 &quot;Use Case 계층이 내 코드에는 없는 것 같은데?&quot;라는 생각이 들었습니다. 하지만 다시 구조를 들여다보면, 이미 Service가 Use Case의 역할을 수행하고 있는 경우가 많습니다.&lt;/p&gt;
&lt;p data-end=&quot;530&quot; data-start=&quot;271&quot; data-ke-size=&quot;size16&quot;&gt;스프링 기반의 전형적인 구조에서는 보통 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Controller &amp;rarr; Service &amp;rarr; Repository&lt;/span&gt; 형태로 계층이 구성됩니다. 이때 우리는 비즈니스 로직을 Service에 작성해왔습니다. 클린 아키텍처 관점에서도 핵심은 동일합니다. 다만 용어가 조금 다를 뿐입니다. &lt;br /&gt;Controller는 Interface Adapter에 가깝고, &lt;br /&gt;Repository는 Gateway의 역할을 하며, &lt;br /&gt;우리가 Service라고 부르던 영역이 사실상 Use Case 계층에 해당합니다.&lt;/p&gt;
&lt;p data-end=&quot;743&quot; data-start=&quot;532&quot; data-ke-size=&quot;size16&quot;&gt;즉, Use Case는 &quot;반드시 새로운 레이어를 추가해야 한다&quot;는 의미가 아니라, 애플리케이션의 특정 기능 흐름을 책임지는 계층을 어떻게 바라볼 것인가에 대한 관점에 가깝습니다. 결국 중요한 것은 이름이 Service냐 Use Case냐가 아니라, 그 코드가 하나의 사용자 행동(기능)을 명확히 표현하고 있는지, 그리고 기술적 세부사항으로부터 얼마나 독립적으로 유지되는지입니다.&lt;/p&gt;
&lt;p data-end=&quot;885&quot; data-start=&quot;745&quot; data-ke-size=&quot;size16&quot;&gt;정리하자면, 제 사례에서 Use Case 계층이 없어 보였던 이유는 이미 Service가 그 역할을 하고 있었기 때문입니다. 다만 클린 아키텍처에서는 이를 더 명확히 &quot;행위 단위&quot;로 분리하고, 비즈니스 규칙을 보호하는 구조로 강화하는 것을 권장합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Interface Adapters&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Interface Adapters는 핵심 로직(Entities, Use Cases)과 외부 시스템을 연결하는 계층입니다. 하지만 단순히 연결하는 통로라기보다는, 서로 다른 계층의 형식을 변환하고 조정하는 역할을 담당합니다.&lt;/p&gt;
&lt;p data-end=&quot;443&quot; data-start=&quot;270&quot; data-ke-size=&quot;size16&quot;&gt;외부 세계는 보통 HTTP 요청, JSON, 메시지, DB 레코드와 같은 형태로 데이터를 주고받습니다. 반면, 내부의 유스케이스와 엔티티는 순수한 객체와 비즈니스 규칙 중심으로 동작합니다. Interface Adapter는 이 둘 사이에서 데이터를 변환하여 핵심 로직이 외부 기술에 오염되지 않도록 보호합니다.&lt;/p&gt;
&lt;p data-end=&quot;472&quot; data-start=&quot;445&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같은 구성 요소들이 해당됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;568&quot; data-start=&quot;474&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;493&quot; data-start=&quot;474&quot;&gt;UI &amp;harr; Controller&lt;/li&gt;
&lt;li data-end=&quot;514&quot; data-start=&quot;494&quot;&gt;API &amp;harr; Controller&lt;/li&gt;
&lt;li data-end=&quot;538&quot; data-start=&quot;515&quot;&gt;Producer &amp;harr; Consumer&lt;/li&gt;
&lt;li data-end=&quot;568&quot; data-start=&quot;539&quot;&gt;Repository 구현체 &amp;harr; Database&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;722&quot; data-start=&quot;570&quot; data-ke-size=&quot;size16&quot;&gt;Controller는 HTTP 요청을 받아 유스케이스가 이해할 수 있는 형태로 전달합니다.&lt;br /&gt;Consumer는 메시지를 받아 내부 로직이 처리할 수 있는 구조로 변환합니다.&lt;br /&gt;Repository 구현체는 데이터베이스와 통신하지만, 유스케이스에는 인터페이스만 노출합니다.&lt;/p&gt;
&lt;p data-end=&quot;814&quot; data-start=&quot;724&quot; data-ke-size=&quot;size16&quot;&gt;즉, Interface Adapter는 외부와 연동할 수 있는 경로이면서 동시에,&lt;br /&gt;핵심 비즈니스 로직을 기술적인 세부사항으로부터 보호하는 방패 역할을 합니다.&lt;br /&gt;핵심은 이것입니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;내부는 외부를 몰라도 된다. &lt;/span&gt;대신 외부가 내부를 이해할 수 있도록 번역해주는 계층이 필요하다.&lt;br /&gt;그 역할을 수행하는 곳이 바로 Interface Adapters입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Frameworks &amp;amp; Drivers (가장 바깥)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Frameworks &amp;amp; Drivers는 시스템의 기술적 세부사항을 담고 있는 가장 바깥 계층입니다.&lt;br /&gt;이 계층에는 프레임워크, 데이터베이스, 외부 API, 메시징 시스템 등과 같은 구현 기술이 포함됩니다.&lt;/p&gt;
&lt;p data-end=&quot;412&quot; data-start=&quot;287&quot; data-ke-size=&quot;size16&quot;&gt;가장 중요한 특징은 변경 가능성이 가장 높은 영역이라는 점입니다.&lt;br /&gt;라이브러리는 버전이 바뀌고, 데이터베이스는 교체되며, 외부 API는 변경됩니다. 이 계층은 언제든지 바뀔 수 있는 기술 요소를 모아두는 영역입니다.&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;414&quot; data-ke-size=&quot;size16&quot;&gt;이 계층의 역할은 애플리케이션이 동작하기 위한 인프라를 제공하는 것입니다.&lt;br /&gt;하지만 여기에는 핵심 비즈니스 로직이 포함되어서는 안 됩니다. 오직 외부 시스템과 상호작용하기 위한 구현 세부사항만 존재해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;414&quot; data-ke-size=&quot;size16&quot;&gt;infra 아래에 다음과 같은 폴더가 있습니다:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgUnEB/dJMcabQQvRD/AQctVxBUO8zXTg2ETU9PqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgUnEB/dJMcabQQvRD/AQctVxBUO8zXTg2ETU9PqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgUnEB/dJMcabQQvRD/AQctVxBUO8zXTg2ETU9PqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgUnEB%2FdJMcabQQvRD%2FAQctVxBUO8zXTg2ETU9PqK%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;253&quot; height=&quot;369&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구성은 전형적인 Frameworks &amp;amp; Drivers 계층입니다.&lt;/p&gt;
&lt;p data-end=&quot;741&quot; data-start=&quot;736&quot; data-ke-size=&quot;size16&quot;&gt;여기에는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;876&quot; data-start=&quot;743&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;771&quot; data-start=&quot;743&quot;&gt;데이터 접근 기술 (jpa, pg, redis)&lt;/li&gt;
&lt;li data-end=&quot;800&quot; data-start=&quot;772&quot;&gt;외부 통신 (gRpc, restTemplate)&lt;/li&gt;
&lt;li data-end=&quot;814&quot; data-start=&quot;801&quot;&gt;인증 기술 (jwt)&lt;/li&gt;
&lt;li data-end=&quot;860&quot; data-start=&quot;815&quot;&gt;인프라 유틸리티 (threadpool, retry, lock, logging)&lt;/li&gt;
&lt;li data-end=&quot;876&quot; data-start=&quot;861&quot;&gt;직렬화 (jackson)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;893&quot; data-start=&quot;878&quot; data-ke-size=&quot;size16&quot;&gt;같은 것들이 모여 있습니다. 이건 정확히 클린 아키텍처가 말하는 &quot;가장 바깥&quot;입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI시대에서 클린아키텍처를 이해를 해야할까 고민이 많았습니다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1772548708316&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;AI가 주니어 개발자를 쓸모없게 만들고 있다 | GeekNews&quot; data-og-description=&quot;AI 도구가 주니어 개발자에게 얕은 역량만 만들어주고 있으며, 코드를 빠르게 출력하지만 왜 그런 접근을 택했는지 설명하지 못하는 상황이 빈번해짐시니어 개발자의 진정한 가치는 코드 작성 &quot; data-og-host=&quot;news.hada.io&quot; data-og-source-url=&quot;https://news.hada.io/topic?id=27162&amp;amp;utm_source=discord&amp;amp;utm_medium=bot&amp;amp;utm_campaign=4262&quot; data-og-url=&quot;https://news.hada.io/topic?id=27162&amp;amp;utm_campaign=4262&amp;amp;utm_medium=bot&amp;amp;utm_source=discord&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bozkjN/dJMb8Zvy4Q5/lZbLFrprtCAmZDUzRtjAIK/img.png?width=1200&amp;amp;height=630&amp;amp;face=367_341_511_498,https://scrap.kakaocdn.net/dn/csJmGJ/dJMb8RjZySB/mymKAfityg7LRPK1cJHnUK/img.png?width=1200&amp;amp;height=630&amp;amp;face=367_341_511_498&quot;&gt;&lt;a href=&quot;https://news.hada.io/topic?id=27162&amp;amp;utm_source=discord&amp;amp;utm_medium=bot&amp;amp;utm_campaign=4262&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://news.hada.io/topic?id=27162&amp;amp;utm_source=discord&amp;amp;utm_medium=bot&amp;amp;utm_campaign=4262&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bozkjN/dJMb8Zvy4Q5/lZbLFrprtCAmZDUzRtjAIK/img.png?width=1200&amp;amp;height=630&amp;amp;face=367_341_511_498,https://scrap.kakaocdn.net/dn/csJmGJ/dJMb8RjZySB/mymKAfityg7LRPK1cJHnUK/img.png?width=1200&amp;amp;height=630&amp;amp;face=367_341_511_498');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;AI가 주니어 개발자를 쓸모없게 만들고 있다 | GeekNews&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AI 도구가 주니어 개발자에게 얕은 역량만 만들어주고 있으며, 코드를 빠르게 출력하지만 왜 그런 접근을 택했는지 설명하지 못하는 상황이 빈번해짐시니어 개발자의 진정한 가치는 코드 작성&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;news.hada.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 읽고 생각이 완전히 바뀌었다는건 아니지만, 뭐랄까 AI가 완전히 개발 시장을 장악하기전까지는 최선을 다해 그들위에 서야 한다고 된다고 느꼈던거 같습니다. 이 글도 AI를 통해 적고 읽고를 반복했습니다. 적다보니 이해가 되더라구요. AI가 쓴글을 바로 복사해서 사용하는것이 아닌 읽고 옮기다보니 새로운 글이 되어지는 느낌이 듭니다.&amp;nbsp;&lt;br /&gt;클린 아키텍처를 보면 대부분 제가 아는것들이었습니다. 다만 용어가 조금 다른경우도 더러있었습니다. 일단 최선을 다할 생각입니다.&amp;nbsp;&lt;br /&gt;하고 싶은게 있는데 그걸 하는게 제일 행복이 아닐까 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://blog.bytebytego.com/p/clean-architecture-101-building-software&quot;&gt;https://blog.bytebytego.com/p/clean-architecture-101-building-software&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/541</guid>
      <comments>https://b-programmer.tistory.com/541#entry541comment</comments>
      <pubDate>Tue, 3 Mar 2026 23:44:43 +0900</pubDate>
    </item>
    <item>
      <title>minikube 간단하게 사용해보기</title>
      <link>https://b-programmer.tistory.com/540</link>
      <description>&lt;figure id=&quot;og_1772362983583&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;무중단 배포를 적용해보자.&quot; data-og-description=&quot;502 게이트웨이를 없애보자. (이론편)배포를 진행하게 되면 일정 시간 동안 딜레이가 발생합니다.현재 화면은 배포 전 상태이며, localhost인 이유는 로컬 환경에서 배포를 진행했기 때문입니다.문&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/538&quot; data-og-url=&quot;https://b-programmer.tistory.com/538&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cCRdst/dJMb8QL9pvd/XHHKHlIKDpQlSKSDzjW6a0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/vwKT1/dJMb8XkcN4j/Mznczo2X1EBH27AIKnRBfk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/SjUAm/dJMb8866iHU/KOdESVY8KlqNGqOncbhrjK/img.jpg?width=383&amp;amp;height=248&amp;amp;face=108_72_174_144&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/538&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/538&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cCRdst/dJMb8QL9pvd/XHHKHlIKDpQlSKSDzjW6a0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/vwKT1/dJMb8XkcN4j/Mznczo2X1EBH27AIKnRBfk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/SjUAm/dJMb8866iHU/KOdESVY8KlqNGqOncbhrjK/img.jpg?width=383&amp;amp;height=248&amp;amp;face=108_72_174_144');&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;무중단 배포를 적용해보자.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;502 게이트웨이를 없애보자. (이론편)배포를 진행하게 되면 일정 시간 동안 딜레이가 발생합니다.현재 화면은 배포 전 상태이며, localhost인 이유는 로컬 환경에서 배포를 진행했기 때문입니다.문&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서 저는 무중단 배포를 실습해 봤습니다. 로드벨런서(nginx)를 이용해서 rolling배포를 진행해 봤습니다. 그리고 쿠버네티스를 이용하면, rolling배포를 조금 더 편하게(?) 사용할 수 있다고 합니다. 그래서 쿠버네티스를 직접 사용해 보면 어떨까 싶었습니다. 하지만 요게 운영환경에서 사용이 되어서 로컬에서 사용하기에는 어렵다고 알고 있습니다. 그래서 찾다 보니 로컬에서는 minikube를 사용하면 쿠버네티스로 개발이 가능하다는군요. 이걸 통해서 실습을 진행해 볼 예정입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;minikube 실행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;minikube를 실행시켜 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772365005043&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;minikube start --driver=docker&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뜻은 minikube를 docker 드라이버로 실행하겠다는 뜻입니다.&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;/h2&gt;
&lt;pre id=&quot;code_1772371874095&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl create deployment hello-node \
--image=registry.k8s.io/e2e-test-images/agnhost:2.53 \
-n test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;281&quot; data-start=&quot;271&quot; data-ke-size=&quot;size16&quot;&gt;테스트를 위해 hello-node라는 것을 배포한다고 하지만, 쿠버네티스에서 이것이 무엇을 의미하는지 알 필요가 있다고 생각합니다.&lt;br /&gt;우리가 일반적으로 말하는 배포는 다음과 같습니다. 서버에 접속해서 jar 파일을 올리고, 프로세스를 실행합니다. 애플리케이션이 죽으면 직접 다시 실행하고, 버전이 바뀌면 기존 파일을 덮어씌워 재실행합니다. 즉, 우리가 말하던 배포는 &quot;애플리케이션을 실행시키는 행위&quot;에 가까웠습니다. 하지만 쿠버네티스에서 말하는 앱 배포는 조금 다릅니다.&lt;/p&gt;
&lt;p data-end=&quot;357&quot; data-start=&quot;338&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;1828&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/culAjO/dJMcagSamwI/CfCGUkIwU1yp6c61BdupKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/culAjO/dJMcagSamwI/CfCGUkIwU1yp6c61BdupKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/culAjO/dJMcagSamwI/CfCGUkIwU1yp6c61BdupKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FculAjO%2FdJMcagSamwI%2FCfCGUkIwU1yp6c61BdupKK%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;654&quot; height=&quot;257&quot; data-origin-width=&quot;1828&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 보면, 앱 배포가 쿠버네티스를 거쳐 여러 개의 파드로 이어지고, 그 파드들이 운영 서버 위에서 실행되는 구조처럼 표현되어 있습니다. 겉으로 보기에는 &quot;파드를 만들고 그것을 운영 서버에 적절하게 배치하는 과정&quot;처럼 보일 수 있습니다.&lt;br /&gt;하지만 이것은 결과일 뿐입니다. &lt;u&gt;쿠버네티스에서 배포란 단순히 파드를 생성하는 행위가 아니라, 해당 애플리케이션이 어떤 상태로 존재해야 하는지를 선언하는 행위&lt;/u&gt;입니다.&lt;br /&gt;예를 들어, hello-node는 항상 1개의 파드로 실행되어야 한다고 선언하면 쿠버네티스는 이를 클러스터의 목표 상태로 기록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표 상태가 어떤지 현재 상태가 무엇인지 확인해보겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;목표 상태&lt;/h4&gt;
&lt;pre id=&quot;code_1772453552232&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: agnhost
        image: registry.k8s.io/e2e-test-images/agnhost:2.53&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;375&quot; data-start=&quot;339&quot;&gt;replicas: 1 &amp;rarr; &quot;파드 1개를 유지해줘&quot;라는 선언&lt;/li&gt;
&lt;li data-end=&quot;413&quot; data-start=&quot;376&quot;&gt;image: ... &amp;rarr; &quot;어떤 앱(이미지)으로 띄울지&quot; 선언&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;현재 상태&lt;/h4&gt;
&lt;pre id=&quot;code_1772453572370&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;status:
  replicas: 1
  readyReplicas: 1
  availableReplicas: 1&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;565&quot; data-start=&quot;525&quot;&gt;readyReplicas: 1 &amp;rarr; 실제로 준비 완료된 파드가 1개&lt;/li&gt;
&lt;li data-end=&quot;608&quot; data-start=&quot;566&quot;&gt;즉, 목표(spec)와 현재(status)가 일치하고 있다는 증거&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그 이후에는 시스템이 현재 상태와 비교합니다. &lt;/span&gt;파드가 없으면 새로 생성하고 개수가 부족하면 추가로 만들며 필요하다면 적절한 노드에 다시 배치합니다.&lt;br /&gt;따라서 쿠버네티스에서의 배포는 직접 애플리케이션을 실행하는 행위라기보다, 애플리케이션의 운영 상태를 시스템에 위임하는 과정이라고 볼 수 있습니다. 혹자는 쿠버네티스의 배포를 &quot;상태를 변경한다&quot;라고 표현합니다. 이는 단순 실행이 아니라, 클러스터가 유지해야 할 목표 상태를 정의하는 행위이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 현재 상태를 목표 상태로 변경하는 작업이 필요할 것이라는 생각이 들 수 있습니다.&lt;br /&gt;하지만 쿠버네티스는 현재 상태를 기준으로 동작하지 않습니다. 항상 &quot;내가 선언한 목표 상태&quot;를 기준으로 삼고, 현재 상태를 그에 맞추는 방식으로 동작합니다. 그렇다면 목표 상태는 어떻게 설정할 수 있을까요?&lt;br /&gt;방법은 여러 가지가 있지만, 가장 단순하게 생각하면 &amp;ldquo;다시 선언하면 된다&amp;rdquo;는 말로 정리할 수 있습니다.&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;2510&quot; data-origin-height=&quot;1356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L9ShS/dJMcafFIyrq/GDlqMvPGmwDFrr7PqtbyUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L9ShS/dJMcafFIyrq/GDlqMvPGmwDFrr7PqtbyUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L9ShS/dJMcafFIyrq/GDlqMvPGmwDFrr7PqtbyUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL9ShS%2FdJMcafFIyrq%2FGDlqMvPGmwDFrr7PqtbyUk%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;547&quot; height=&quot;296&quot; data-origin-width=&quot;2510&quot; data-origin-height=&quot;1356&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 hello-node 이미지는 기본적으로 정식 HTTP 서버로 동작하지 않습니다.&lt;br /&gt;따라서 HTTP 요청을 확인하기 위해 실행 옵션을 추가해주어야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;211&quot; data-start=&quot;138&quot; data-ke-size=&quot;size16&quot;&gt;아래 명령은 디플로이먼트에 실행 인자를 추가하여, 컨테이너가 HTTP 테스트 서버 모드로 동작하도록 설정하는 과정입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1772455382033&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl patch deployment hello-node --type='json' -p='[
  {&quot;op&quot;:&quot;add&quot;,&quot;path&quot;:&quot;/spec/template/spec/containers/0/args&quot;,&quot;value&quot;:[&quot;netexec&quot;,&quot;--http-port=8080&quot;]}
]'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1772455488223&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;deployment.apps/hello-node patched&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경했습니다. 이제 hello-node는 port가 8080입니다. 그러니까 http라는거죠&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;2502&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ec0ecA/dJMcaiWHEVB/07sHkg77w49fcXzqaei9p0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ec0ecA/dJMcaiWHEVB/07sHkg77w49fcXzqaei9p0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ec0ecA/dJMcaiWHEVB/07sHkg77w49fcXzqaei9p0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fec0ecA%2FdJMcaiWHEVB%2F07sHkg77w49fcXzqaei9p0%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;2502&quot; height=&quot;448&quot; data-origin-width=&quot;2502&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubectl patch 명령을 실행한 이후, ReplicaSet이 두 개로 보이는 현상이 발생했습니다.&lt;br /&gt;그 이유는 Deployment의 파드 템플릿(spec.template) 이 변경되었기 때문입니다.&lt;/p&gt;
&lt;p data-end=&quot;279&quot; data-start=&quot;223&quot; data-ke-size=&quot;size16&quot;&gt;쿠버네티스는 Deployment의 스펙이 달라지면 &quot;새로운 버전이 배포되어야 한다&quot;고 판단합니다.&lt;br /&gt;즉, 단순히 값을 수정하는 것이 아니라 새로운 버전으로 다시 배포하는 과정이 시작됩니다.&lt;/p&gt;
&lt;h3 data-end=&quot;358&quot; data-start=&quot;343&quot; data-ke-size=&quot;size23&quot;&gt;내부적으로 일어나는 일&lt;/h3&gt;
&lt;p data-end=&quot;393&quot; data-start=&quot;360&quot; data-ke-size=&quot;size16&quot;&gt;스펙이 변경되면 쿠버네티스는 다음과 같은 과정을 수행합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;488&quot; data-start=&quot;395&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;421&quot; data-start=&quot;395&quot;&gt;새로운 ReplicaSet을 생성한다.&lt;/li&gt;
&lt;li data-end=&quot;439&quot; data-start=&quot;422&quot;&gt;새 Pod를 생성한다.&lt;/li&gt;
&lt;li data-end=&quot;464&quot; data-start=&quot;440&quot;&gt;기존 Pod를 점진적으로 종료한다.&lt;/li&gt;
&lt;li data-end=&quot;488&quot; data-start=&quot;465&quot;&gt;최종적으로 목표 개수만 유지한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;531&quot; data-start=&quot;490&quot; data-ke-size=&quot;size16&quot;&gt;이 과정을 롤링 업데이트(Rolling Update) 라고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서비스의 등장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 디플로이먼트를 만들었고 파드가 정상적으로 떠있는것을 확인했습니다. 잠시 위의 그림을 소환해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1828&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/culAjO/dJMcagSamwI/CfCGUkIwU1yp6c61BdupKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/culAjO/dJMcagSamwI/CfCGUkIwU1yp6c61BdupKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/culAjO/dJMcagSamwI/CfCGUkIwU1yp6c61BdupKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FculAjO%2FdJMcagSamwI%2FCfCGUkIwU1yp6c61BdupKK%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;654&quot; height=&quot;257&quot; data-origin-width=&quot;1828&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림을 보면 마치 Pod가 곧바로 운영 서버에 배포되어 바로 외부 요청을 처리하는 것처럼 보일 수 있습니다.&lt;br /&gt;하지만 실제로는 그렇지 않습니다. 파드는 단지 &quot;실행 단위&quot;일 뿐이며, 외부에서 직접 접근하기 위한 고정된 접점이 아닙니다.&lt;br /&gt;파드는 언제든지 재 생성될 수 있고, IP 또한 변경될 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;76&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6Hndj/dJMcad181Br/4NWvXuZ67AqJkwTzhqMzgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6Hndj/dJMcad181Br/4NWvXuZ67AqJkwTzhqMzgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6Hndj/dJMcad181Br/4NWvXuZ67AqJkwTzhqMzgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6Hndj%2FdJMcad181Br%2F4NWvXuZ67AqJkwTzhqMzgK%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;1992&quot; height=&quot;76&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;76&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Pod가 떠 있다고 해서 곧바로 외부 요청을 안정적으로 받을 수 있는 상태는 아닙니다.&lt;/p&gt;
&lt;p data-end=&quot;217&quot; data-start=&quot;115&quot; data-ke-size=&quot;size16&quot;&gt;여기서 등장하는 개념이 바로 서비스입니다. 서비스는 파드 앞에 위치하는 고정된 접속 지점으로, 외부 요청을 받아 내부의 파드로 전달하는 역할을 합니다. 즉, 디플로이먼트는 애플리케이션을 실행시키는 역할을 하고, 서비스는 실행된 애플리케이션에 접근할 수 있도록 연결해주는 역할을 합니다. 따라서 서비스를 생성하는 이유는 단순히 리소스를 하나 더 만드는 것이 아니라,&lt;br /&gt;실제로 애플리케이션에 접근할 수 있는 통로를 마련하는 과정이라고 볼 수 있습니다.&lt;br /&gt;결국 서비스는 외부 요청이 파드까지 도달할 수 있도록 연결해주는 통로라고 이해하면 됩니다. 파드는 언제든지 재생성될 수 있고, IP 또한 변경될 수 있기 때문에 안정적인 접점이 아닙니다. 따라서 이러한 변화를 중앙에서 흡수하고, 항상 동일한 방식으로 접근할 수 있도록 해주는 역할이 필요하며, 그 역할을 서비스가 담당합니다.&lt;/p&gt;
&lt;p data-end=&quot;217&quot; data-start=&quot;115&quot; data-ke-size=&quot;size16&quot;&gt;한번 실행해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772459857674&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl expose deployment hello-node --type=LoadBalancer --port=8080&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2508&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0bMRN/dJMcafFIzOy/tE2F623zp7Mv0vTXHuKTm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0bMRN/dJMcafFIzOy/tE2F623zp7Mv0vTXHuKTm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0bMRN/dJMcafFIzOy/tE2F623zp7Mv0vTXHuKTm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0bMRN%2FdJMcafFIzOy%2FtE2F623zp7Mv0vTXHuKTm1%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;2508&quot; height=&quot;256&quot; data-origin-width=&quot;2508&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 올라갔다는것이 보여집니다. &lt;br /&gt;&lt;br /&gt;그렇다면, Type은 무엇이며, ClusterIP, 내부 엔드 포인트는 무엇을 말하는 걸까요? 그리고 외부 엔드 포인트는 아무것도 없을까요?&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Type&lt;/h4&gt;
&lt;p data-end=&quot;317&quot; data-start=&quot;264&quot; data-ke-size=&quot;size16&quot;&gt;Type은 서비스의 접근 방식을 의미합니다. 대표적으로 ClusterIP, NodePort, LoadBalancer가 존재합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;289&quot; data-start=&quot;159&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;289&quot; data-start=&quot;245&quot;&gt;&lt;b&gt;ClusterIP&lt;/b&gt;&amp;nbsp;외부에 노출할 필요가 없는 내부 서비스 용도로 사용됩니다.&lt;/li&gt;
&lt;li data-end=&quot;289&quot; data-start=&quot;245&quot;&gt;&lt;b&gt;NodePort&lt;/b&gt;&amp;nbsp;간단한 외부 접근 테스트 용도로 사용됩니다.&lt;/li&gt;
&lt;li data-end=&quot;289&quot; data-start=&quot;245&quot;&gt;&lt;b&gt;LoadBalancer&lt;/b&gt;&amp;nbsp;운영 환경에서 외부에 공개해야 하는 서비스에 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;353&quot; data-start=&quot;291&quot; data-ke-size=&quot;size16&quot;&gt;Minikube는 클라우드 환경이 아니기 때문에, 실제 외부 LoadBalancer는 존재하지 않습니다.&lt;br /&gt;따라서 서비스의 Type을 LoadBalancer로 설정하더라도 EXTERNAL-IP는 &amp;lt;pending&amp;gt; 상태로 표시됩니다.&lt;br /&gt;이는 오류가 아니라, 외부 LoadBalancer를 생성할 수 없는 로컬 환경의 특성 때문입니다.&lt;/p&gt;
&lt;h4 data-end=&quot;317&quot; data-start=&quot;264&quot; data-ke-size=&quot;size20&quot;&gt;ClusterIP&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 가지는 고정된 내부 IP입니다. 그림으로 그려보면 대략적으로 다음과 같은 그림 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bS9qri/dJMcahDvxP0/Fejmb6o5Rb3MeZyIoT0fLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bS9qri/dJMcahDvxP0/Fejmb6o5Rb3MeZyIoT0fLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bS9qri/dJMcahDvxP0/Fejmb6o5Rb3MeZyIoT0fLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbS9qri%2FdJMcahDvxP0%2FFejmb6o5Rb3MeZyIoT0fLk%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;498&quot; height=&quot;323&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;736&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 현재 실행 중인 파드로 트래픽을 전달하는 역할을 합니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;567&quot; data-end=&quot;597&quot; data-ke-size=&quot;size20&quot;&gt;Endpoints&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 실제로 연결하고 있는 파드 목록입니다.&amp;nbsp;&lt;br /&gt;하지만 여기서 조금 이상하게 느껴질 수 있습니다. 저는 분명 파드를 하나만 실행했고, 실행 포트도 8080으로 변경했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2498&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oBA2M/dJMcaadlFXW/kn2kQnjIioSq9lk9JtCTkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oBA2M/dJMcaadlFXW/kn2kQnjIioSq9lk9JtCTkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oBA2M/dJMcaadlFXW/kn2kQnjIioSq9lk9JtCTkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoBA2M%2FdJMcaadlFXW%2Fkn2kQnjIioSq9lk9JtCTkk%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;2498&quot; height=&quot;296&quot; data-origin-width=&quot;2498&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 실행 중인 파드도 하나뿐입니다. 그렇다면 서비스 쪽에서는 이전 파드의 기록을 제거하지 않고 남겨두는 것처럼 보일 수 있습니다.&lt;br /&gt;하지만 실제로는 그렇지 않습니다. 서비스는 파드를 기록하는 것이 아니라, 현재 라벨 조건에 맞는 파드를 실시간으로 조회하여 연결 대상을 구성합니다. 즉, Endpoints에 표시되는 값은 과거의 기록이 아니라, &lt;u&gt;지금 이 순간 실제로 연결 가능한 파드의 정보(IP:Port) &lt;/u&gt;입니다.&lt;br /&gt;파드가 재생성되거나 IP가 변경되면 Endpoints 또한 자동으로 갱신됩니다. &lt;br /&gt;따라서 Endpoints는 누적되는 기록이 아니라, 현재 상태를 반영한 연결 대상 목록이라고 이해하면 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;외부 Endpoints가 존재하지 않는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;minikube 환경에서는 진짜 클라우드 LoadBalancer가 존재하지 않기 때문에 EXTERNAL-IP가 비어있거나 &amp;lt;pending&amp;gt;으로 나옵니다.&lt;br /&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;포트 포워딩이란?&lt;br /&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;/li&gt;
&lt;li&gt;애플리케이션이 로컬로 이동하는 것이 아니라, 로컬과 클러스터 사이에 통로를 만들어주는 방식&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 명령어로 사용이 되어집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772462665173&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl port-forward service/hello-node 18080:8080&lt;/code&gt;&lt;/pre&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;656&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvKKPr/dJMcaflpCF2/PajCZjNYqWkKLxLs9LQzp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvKKPr/dJMcaflpCF2/PajCZjNYqWkKLxLs9LQzp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvKKPr/dJMcaflpCF2/PajCZjNYqWkKLxLs9LQzp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvKKPr%2FdJMcaflpCF2%2FPajCZjNYqWkKLxLs9LQzp0%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;656&quot; height=&quot;68&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이렇게 만든 앱을 로컬에서도 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;78&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNfmJD/dJMcac90a8a/VMtva2tfFK2ECSdI3547bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNfmJD/dJMcac90a8a/VMtva2tfFK2ECSdI3547bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNfmJD/dJMcac90a8a/VMtva2tfFK2ECSdI3547bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNfmJD%2FdJMcac90a8a%2FVMtva2tfFK2ECSdI3547bk%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;1236&quot; height=&quot;78&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;78&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 분명 18080을 배포한적이 없지만 쿠버네티스에서 포트포워딩한 결과로 사용할 수 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 minikube를 사용해봤습니다. 쿠버네티스는 어떻게 앱을 배포하고 그것을 어떻게 로컬에서 사용하는거까지 사용해본느낌입니다.&amp;nbsp;&lt;br /&gt;이제 본견적으로 디플로이먼트, 파드, 레플리케이션등 기본적인 개념도 알아보고, 제가 직접 개발하고 있는 앱도 올려보는 시간도 가져보겠습니다. 회사에서 일했을때 alroCD를 이용했던적이 있었는데 그것은 쿠버네티스를 사용해야지 이용할 수 있다고 합니다. 이것도 추후에 다뤄보겠습니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/540</guid>
      <comments>https://b-programmer.tistory.com/540#entry540comment</comments>
      <pubDate>Mon, 2 Mar 2026 23:55:23 +0900</pubDate>
    </item>
    <item>
      <title>완벽한 일관성은 존재하지 않는다.</title>
      <link>https://b-programmer.tistory.com/539</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;DB만 사용할 때는 들어오는 데이터와 나가는 데이터가 크게 다르지 않습니다. 하나의 서버가 하나의 상태를 관리하고 있기 때문입니다.&lt;br /&gt;모든 요청은 동일한 저장소를 바라보고, 하나의 진실(Source of Truth)이 존재합니다. 하지만 서비스의 규모가 커지는 순간 상황은 달라집니다. 트래픽을 감당하기 위해 서버를 늘리고, 응답 속도를 개선하기 위해 Redis를 도입하고, 비동기 처리를 위해 Kafka를 붙입니다.&lt;br /&gt;그리고 그 순간부터 우리는 불편한 사실과 마주하게 됩니다. 어떤 곳에는 최신 데이터가 있고, 어떤 곳에는 아직 반영되지 않은 값이 있습니다. 어떤 요청은 성공했는데, 다른 곳에서는 여전히 이전 상태를 반환합니다. 데이터의 불일치는 사라지지 않았습니다. 오히려 더 자주, 더 복잡한 형태로 나타납니다. 그렇다면 우리는 Redis나 Kafka를 잘못 사용하고 있는 걸까요?&lt;/p&gt;
&lt;figure id=&quot;og_1772199363265&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;Eventual Consistency: The Key Trade-Off Behind Modern Databases&quot; data-og-description=&quot;In this article, we will look at what eventual consistency is, why it exists, how to control it, and how to handle the challenges it creates.&quot; data-og-host=&quot;blog.bytebytego.com&quot; data-og-source-url=&quot;https://blog.bytebytego.com/p/eventual-consistency-the-key-trade&quot; data-og-url=&quot;https://blog.bytebytego.com/p/eventual-consistency-the-key-trade&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c9wtAE/dJMb8ZvyK6u/BUCaytMb2naQECHT79U3P0/img.jpg?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675,https://scrap.kakaocdn.net/dn/s6VZ5/dJMb8PGtt8l/67RRwNblONV6CxgwKIZ1i1/img.jpg?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800,https://scrap.kakaocdn.net/dn/yQeT9/dJMb8PGtt8j/SI0zra9xQqhFtm4q6wFpj1/img.jpg?width=1456&amp;amp;height=1698&amp;amp;face=0_0_1456_1698&quot;&gt;&lt;a href=&quot;https://blog.bytebytego.com/p/eventual-consistency-the-key-trade&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.bytebytego.com/p/eventual-consistency-the-key-trade&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c9wtAE/dJMb8ZvyK6u/BUCaytMb2naQECHT79U3P0/img.jpg?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675,https://scrap.kakaocdn.net/dn/s6VZ5/dJMb8PGtt8l/67RRwNblONV6CxgwKIZ1i1/img.jpg?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800,https://scrap.kakaocdn.net/dn/yQeT9/dJMb8PGtt8j/SI0zra9xQqhFtm4q6wFpj1/img.jpg?width=1456&amp;amp;height=1698&amp;amp;face=0_0_1456_1698');&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;Eventual Consistency: The Key Trade-Off Behind Modern Databases&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;In this article, we will look at what eventual consistency is, why it exists, how to control it, and how to handle the challenges it creates.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.bytebytego.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eventual Consistency에 대한 글에서는 그렇지 않다고 말합니다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;문제는 기술의 사용법이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;분산이라는 선택 그 자체에 있다고 이야기합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그 전에 데이터 일관성이란 무엇일까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷에 데이터 일관성을 검색해보니 다음과 같은 답변이 나왔습니다.&lt;br /&gt;&lt;br /&gt;데이터 일관성은 모든 사본 또는 인스턴스가 모든 시스템 및 데이터베이스에서 동일한 데이터 상태를 나타냅니다. 일관성은 다양한 데이터베이스 시스템, 애플리케이션 및 플랫폼에서 데이터가 정확하고 최신이며 일관성이 있는지 확인하는 데 도움이 됩니다.&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;i&gt;&amp;nbsp; google search&lt;br /&gt;&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그렇다는건 데이터가 들어올때와 저장이 되어질때의 데이터가 일치를 해야 한다는 뜻이라는것을 알 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;430&quot; data-start=&quot;406&quot; data-ke-size=&quot;size26&quot;&gt;우리는 무엇을 얻고, 무엇을 포기했는가&lt;/h2&gt;
&lt;p data-end=&quot;485&quot; data-start=&quot;432&quot; data-ke-size=&quot;size16&quot;&gt;우리는 Redis를 사용하면서 속도를 얻었습니다. 하지만 동시에 안전성을 일부 포기했습니다.&lt;br /&gt;Redis는 매우 빠른 시스템입니다. 메모리 기반이며, 조회 성능은 뛰어납니다.&lt;br /&gt;그러나 Redis는 애초에 데이터의 절대적인 정확성을 보장하기 위해 설계된 시스템은 아닙니다.&lt;br /&gt;강한 정합성과 영속성을 최우선으로 하는 전통적인 데이터베이스와는 목적이 다릅니다.&lt;br /&gt;그럼에도 불구하고 우리는 Redis에게 속도도 요구하고, 데이터 유실도 없어야 하며, 항상 최신 상태이기를 기대합니다.&lt;br /&gt;문제는 기술이 아니라 우리의 기대일지도 모릅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모든것은 의도가 있어야 한다.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술은 특정 문제를 해결하기 위해 등장합니다. 그 기술이 가진 한계를 억지로 덮기 위해&lt;br /&gt;본래 목적과 다른 방식으로 일관성을 강요할 필요는 없다고 생각합니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Redis는 속도를 위해 존재합니다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Kafka는 비동기 처리를 위해 설계되었습니다.&lt;br /&gt;&lt;/span&gt;각 기술은 명확한 목적과 전제가 있습니다.&lt;br /&gt;그 한계를 인정하지 않은 채 모든 계층에서 완벽한 정합성을 맞추려는 시도는&lt;br /&gt;오히려 설계를 복잡하게 만들 뿐입니다.&lt;br /&gt;중요한 것은 한계를 부정하는 것이 아니라, 그 한계를 이해하고 어떻게 다룰 것인지 고민하는 일입니다.&lt;br /&gt;그리고 그 고민이 구조로 표현되어야 합니다. 재현 가능하고 설명 가능한 형태로 정리되어야 합니다.&lt;br /&gt;우리는 그것을 패턴이라고 부릅니다.&lt;br /&gt;패턴은 한계를 부정한 결과가 아니라, 한계를 인정한 설계의 결과입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 기술을 어떻게 이해하고 표현하느냐라고 생각합니다. 그리고 그 기술이 가진 한계점을 정확히 인지하는 것이 무엇보다 중요합니다. 기술의 한계를 억지로 덮어 문제를 해결하려 하기보다는, 그 한계를 인정하고 설계에 반영하는 태도가 필요합니다. 어떤 기술은 속도를 위해 존재하고, 어떤 기술은 정합성을 위해 존재합니다. 모든 기술이 모든 문제를 해결해주지는 않습니다. 기술이 가진 전제와 한계를 이해한 상태에서 적절한 위치에 배치한다면, 그 기술은 훨씬 효과적으로 사용될 수 있다고 생각합니다. 완벽함을 강요하는 대신, 의도를 분명히 하고 한계를 설계로 다루는 것. 그것이 분산 환경에서 우리가 가져야 할 태도라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/539</guid>
      <comments>https://b-programmer.tistory.com/539#entry539comment</comments>
      <pubDate>Fri, 27 Feb 2026 23:20:31 +0900</pubDate>
    </item>
    <item>
      <title>무중단 배포를 적용해보자.</title>
      <link>https://b-programmer.tistory.com/538</link>
      <description>&lt;figure id=&quot;og_1772104945756&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;502 게이트웨이를 없애보자. (이론편)&quot; data-og-description=&quot;배포를 진행하게 되면 일정 시간 동안 딜레이가 발생합니다.현재 화면은 배포 전 상태이며, localhost인 이유는 로컬 환경에서 배포를 진행했기 때문입니다.문제는 배포가 완료되는 과정에서 잠시&quot; data-og-host=&quot;b-programmer.tistory.com&quot; data-og-source-url=&quot;https://b-programmer.tistory.com/536&quot; data-og-url=&quot;https://b-programmer.tistory.com/536&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Hogx5/dJMb9fZswp3/861ChwUV35sfxP2K3q8FcK/img.png?width=800&amp;amp;height=166&amp;amp;face=0_0_800_166,https://scrap.kakaocdn.net/dn/dIMWbP/dJMb85vL66B/KDDn0F8qoPIKUgBaEc983k/img.png?width=800&amp;amp;height=166&amp;amp;face=0_0_800_166,https://scrap.kakaocdn.net/dn/bGUPLX/dJMb9kT0aFx/LQ3Ytjh9yUQiFWvxSqLjo0/img.png?width=1596&amp;amp;height=764&amp;amp;face=0_0_1596_764&quot;&gt;&lt;a href=&quot;https://b-programmer.tistory.com/536&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://b-programmer.tistory.com/536&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Hogx5/dJMb9fZswp3/861ChwUV35sfxP2K3q8FcK/img.png?width=800&amp;amp;height=166&amp;amp;face=0_0_800_166,https://scrap.kakaocdn.net/dn/dIMWbP/dJMb85vL66B/KDDn0F8qoPIKUgBaEc983k/img.png?width=800&amp;amp;height=166&amp;amp;face=0_0_800_166,https://scrap.kakaocdn.net/dn/bGUPLX/dJMb9kT0aFx/LQ3Ytjh9yUQiFWvxSqLjo0/img.png?width=1596&amp;amp;height=764&amp;amp;face=0_0_1596_764');&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;502 게이트웨이를 없애보자. (이론편)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;배포를 진행하게 되면 일정 시간 동안 딜레이가 발생합니다.현재 화면은 배포 전 상태이며, localhost인 이유는 로컬 환경에서 배포를 진행했기 때문입니다.문제는 배포가 완료되는 과정에서 잠시&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;b-programmer.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무중단 배포에는 대표적으로 세 가지 방법이 있습니다. Rolling, Blue-Green, Canary 방식입니다. Rolling 배포는 인스턴스를 하나씩 교체하는 방식으로, 서비스 중단 없이 배포가 가능하다는 장점이 있습니다. 하지만 배포 과정에서 구버전과 신버전이 동시에 존재할 수 있어 버전 불일치 문제가 발생할 수 있습니다. 이러한 문제를 보완하기 위해 등장한 방식이 Blue-Green 배포입니다. 기존 환경과 새로운 환경을 동시에 준비한 뒤, 트래픽을 한 번에 전환하는 방식입니다. 하지만 이 방법 역시 새로운 환경에 치명적인 버그가 존재할 경우 전체 사용자에게 동시에 영향을 줄 수 있다는 위험이 존재합니다. 이를 해결하기 위해 일부 사용자에게 먼저 배포하여 안정성을 검증하는 Canary 배포가 등장했습니다.&lt;/p&gt;
&lt;p data-end=&quot;608&quot; data-start=&quot;515&quot; data-ke-size=&quot;size16&quot;&gt;이번 파트에서는 이 세 가지 전략 중 Rolling과 Blue-Green 배포를 직접 실습해 보며, 우리 환경에 어떻게 적용할 수 있을지 고민해 보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;rolling 배포&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rolling 배포는 인스턴스를 하나씩 교체하며 점진적으로 업데이트하는 방식입니다.&lt;br /&gt;쿠버네티스에서는 RollingUpdate가 기본 전략으로 제공되기 때문에, 비교적 쉽게 롤링 배포를 적용할 수 있습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하지만 저는 현재 쿠버네티스를 사용하고 있지 않습니다. 그렇다면 롤링 배포를 적용할 방법이 없을까요?&lt;br /&gt;&lt;/span&gt;결론부터 말하면, 가능합니다. 쿠버네티스가 자동으로 처리해 주는 인스턴스 교체 과정을,&lt;br /&gt;우리가 직접 도커 기반으로 구성하고 트래픽을 제어하면 됩니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Rolling 배포를 위해서는 최소 &lt;/span&gt;두 개 이상의 인스턴스&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;가 필요합니다.&lt;br /&gt;&lt;/span&gt;배포 중에도 항상 하나 이상의 인스턴스가 요청을 처리해야 하기 때문입니다.&lt;br /&gt;따라서 이번 실습에서는 Docker(Compose)를 이용해 동일한 애플리케이션 인스턴스를 두 개 띄우는 것부터 시작해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Docker compose를 확인해 봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1772105577285&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  dragons-api:
    build:
      context: .
      dockerfile: Dockerfile
    image: dragons:${PROFILE:-dev}
    container_name: dragons
    restart: unless-stopped

    ports:
      - &quot;8083:8083&quot;

    env_file:
      - .env&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스가 2개 이상 필요하기 때문에 단순히 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;docker compose down&lt;/span&gt; 후 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;up&lt;/span&gt; 하는 방식으로는,&lt;br /&gt;배포 순간에 애플리케이션이 내려가면서 502가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;402&quot; data-start=&quot;258&quot; data-ke-size=&quot;size16&quot;&gt;따라서 먼저 동일한 애플리케이션을 두 개의 인스턴스로 실행할 수 있도록 구성해야 합니다.&lt;br /&gt;가장 단순한 방법은 기존 dragons-api를 복제하여 dragons-api-1, dragons-api-2처럼 두 개의 서비스로 나누는 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;503&quot; data-start=&quot;404&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같이 컨테이너 이름과 포트만 분리해 두 인스턴스를 동시에 띄울 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772106617426&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  dragons-api1:
    build:
      context: .
      dockerfile: Dockerfile
    image: dragons:${PROFILE:-dev}
    container_name: dragons1
    restart: unless-stopped
    ports:
      - &quot;8083:8083&quot;
    env_file:
      - .env

  dragons-api2:
    build:
      context: .
      dockerfile: Dockerfile
    image: dragons:${PROFILE:-dev}
    container_name: dragons2
    restart: unless-stopped
    ports:
      - &quot;8084:8083&quot;
    env_file:
      - .env&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 인스턴스가 2개가 되었습니다. 이렇게 해서 띄운다고 해서 rolling배포가 되는 것은 아닙니다.&lt;br /&gt;그전에 정상적으로 인스턴스가 띄워졌는지 확인해 봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2450&quot; data-origin-height=&quot;118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZ2uNX/dJMb99SZSQJ/kkfRQJqhYkJwPn0GTYgNzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZ2uNX/dJMb99SZSQJ/kkfRQJqhYkJwPn0GTYgNzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZ2uNX/dJMb99SZSQJ/kkfRQJqhYkJwPn0GTYgNzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZ2uNX%2FdJMb99SZSQJ%2FkkfRQJqhYkJwPn0GTYgNzK%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;2450&quot; height=&quot;118&quot; data-origin-width=&quot;2450&quot; data-origin-height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 올라와있는 것을 확인할 수 있었습니다.&lt;br /&gt;지금 단계에서는 단순히 인스턴스를 2개 띄운 것뿐입니다. 그렇다면 여기서 어떻게 Rolling 배포가 가능해질까요?&lt;br /&gt;Rolling 배포를 하려면, 사용자 요청이 특정 인스턴스 하나에만 고정되지 않고 여러 인스턴스로 분산될 수 있어야 합니다.&lt;br /&gt;즉, 앞단에서 트래픽을 분산해 주는 로드밸런싱이 필요합니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하지만 저는 별도의 로드밸런서를 사용하고 있지 않습니다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;대신 &lt;/span&gt;리버스 프락시 서버(Nginx)를&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 사용하고 있습니다.&lt;br /&gt;&lt;/span&gt;리버스 프락시는 서버 앞단에서 요청을 받아, 내부 애플리케이션 서버로 전달하는 역할을 합니다.&lt;br /&gt;그리고 설정에 따라 여러 인스턴스로 트래픽을 분산시킬 수 있기 때문에,&lt;br /&gt;결과적으로 로드밸런서 역할도 함께 수행할 수 있습니다.&lt;br /&gt;즉, 이번 실습에서는 별도의 로드밸런서 대신 Nginx를 이용해 트래픽을 분산시켜 Rolling 배포를 구현해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 nginx의 upstream기능을 사용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;nginx upstream&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 말해서 upstream은 여러 서버(인스턴스)를 하나의 서비스 풀로 묶는 설정입니다.&lt;br /&gt;그렇다면 어떻게 적용할 수 있을까요? Nginx 설정 파일을 다음과 같이 변경하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772109950257&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http {
    upstream dragons {
        server dragons1:8083;
        server dragons2:8083;
    }

    # -------------------------
    # HTTPS
    # -------------------------
    server {
        ...
       # }

        # -------------------------
        # API (backend)
        # -------------------------
        location /api {
           proxy_next_upstream error timeout http_502 http_503 http_504;  
           proxy_pass http://dragons;
        ...
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 proxy_pass의 대상이 기존에는 특정 컨테이너(서버)를 직접 가리키는 방식이었다면,&lt;br /&gt;이제는 upstream 그룹 이름(dragons)을 바라보도록 변경되었습니다.&lt;br /&gt;즉, nginx가 dragons라는 서비스 풀에 포함된 인스턴스들로 요청을 분산하게 됩니다.&lt;br /&gt;또한 proxy_next_upstream 설정을 통해 특정 인스턴스가 일시적으로 응답하지 못하는 경우,&lt;br /&gt;다른 인스턴스로 요청을 재시도하여 502 발생 가능성을 줄일 수 있습니다.&lt;br /&gt;이렇게 되면 Rolling 배포를 진행하기 위한 준비 과정은 완료되었습니다.&lt;br /&gt;이제 본격적으로 배포 테스트를 진행해 봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1772110886669&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while true; do
curl -k -s -o /dev/null -w &quot;%{http_code}\n&quot; https://localhost/api
sleep 0.3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 통해 /api 요청을 지속적으로 보내보겠습니다.&lt;br /&gt;요청 결과로 401이 출력되었습니다. 이는 서버가 죽었다는 의미가 아니라, 인증이 필요한 API에 로그인 없이 접근했기 때문에 발생한 것으로 추측됩니다.&lt;/p&gt;
&lt;p data-end=&quot;482&quot; data-start=&quot;409&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 여기서 dragons1 인스턴스를 종료하면 어떻게 될까요? 502가 발생할까요, 아니면 여전히 401이 발생할까요?&lt;/p&gt;
&lt;p data-end=&quot;583&quot; data-start=&quot;484&quot; data-ke-size=&quot;size16&quot;&gt;결과적으로 401이 계속 발생했습니다.&lt;br /&gt;즉, 한쪽 인스턴스를 종료하더라도 Nginx가 다른 인스턴스로 요청을 전달하면서 서비스가 유지되고 있음을 확인할 수 있었습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;674&quot; data-start=&quot;585&quot; data-ke-size=&quot;size26&quot;&gt;Bule-Green 배포&lt;/h2&gt;
&lt;p data-end=&quot;178&quot; data-start=&quot;65&quot; data-ke-size=&quot;size16&quot;&gt;Rolling 배포가 같은 서비스 풀 안에서 인스턴스를 하나씩 교체하는 방식이라면, Blue-Green 배포는 두 개의 세트를 동시에 실행해 두고,&lt;br /&gt;전환 순간에 하나의 세트만 활성화하는 방식입니다.&lt;br /&gt;&lt;br /&gt;즉, 기존 환경(Blue)과 새로운 환경(Green)을 동시에 준비해 두고, 트래픽을 한 번에 전환하는 전략입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 여기서 이런 의문이 생길 수 있습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그렇다면 Docker Compose에서 만들어지는 인스턴스와 큰 차이가 없는 것 아닌가?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 물리적인 구성만 놓고 보면 크게 다르지 않습니다. 컨테이너를 두 개 이상 띄운다는 점에서는 Rolling과 유사해 보일 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;449&quot; data-start=&quot;415&quot; data-ke-size=&quot;size16&quot;&gt;하지만 차이는 어디에 트래픽을 보내느냐에 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;544&quot; data-start=&quot;451&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;494&quot; data-start=&quot;451&quot;&gt;Rolling은 두 인스턴스를 동시에 활성화한 상태에서 하나씩 교체합니다.&lt;/li&gt;
&lt;li data-end=&quot;544&quot; data-start=&quot;495&quot;&gt;Blue-Green은 두 세트를 모두 띄워두되, 항상 한 세트만 활성화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;600&quot; data-start=&quot;546&quot; data-ke-size=&quot;size16&quot;&gt;즉, 구조는 비슷해 보이지만 트래픽 제어 방식과 배포 철학이 다르다는 점이 핵심입니다.&lt;/p&gt;
&lt;p data-end=&quot;600&quot; data-start=&quot;546&quot; data-ke-size=&quot;size16&quot;&gt;인스턴스는 다음과 같이 진행하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772113746849&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  dragons-api1:
    build:
      context: .
      dockerfile: Dockerfile
    image: dragons:${PROFILE:-dev}
    container_name: dragons1
    restart: unless-stopped
    ports:
      - &quot;8083:8083&quot;
    env_file:
      - .env

  dragons-api2:
    build:
      context: .
      dockerfile: Dockerfile
    image: dragons:${PROFILE:-dev}
    container_name: dragons2
    restart: unless-stopped
    ports:
      - &quot;8084:8083&quot;
    env_file:
      - .env​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blue-Green 배포 방식은 트래픽을 전환하는 방식입니다.&lt;br /&gt;즉, 새로운 버전을 배포할 때 기존 서버를 하나씩 교체하는 것이 아니라, 트래픽이 향하는 대상을 바꾸는 것이 핵심입니다.&lt;/p&gt;
&lt;p data-end=&quot;288&quot; data-start=&quot;167&quot; data-ke-size=&quot;size16&quot;&gt;이를 위해 서비스 풀을 Blue(기존)와 Green(신규) 두 세트로 나누어 준비해야 합니다.&lt;br /&gt;그리고 전환 순간에는 두 세트 중 하나의 풀만 활성화하여 사용자 트래픽을 처리하도록 구성합니다.&lt;/p&gt;
&lt;p data-end=&quot;288&quot; data-start=&quot;167&quot; data-ke-size=&quot;size16&quot;&gt;저는 nginx_blue.conf와 nginx_green.conf로 구분을 짓겠습니다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;i&gt;nginx_blue.conf&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772115912937&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;upstream dragons {
   server dragons1:8083;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;288&quot; data-start=&quot;167&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;nginx_green.conf&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772115935790&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;upstream dragons {
   server dragons2:8083;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;288&quot; data-start=&quot;167&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;핵심은&lt;br /&gt;기존 nginx.conf에서&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772116976961&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;include /etc/nginx/conf.d/active.conf;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;288&quot; data-start=&quot;167&quot; data-ke-size=&quot;size16&quot;&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;288&quot; data-start=&quot;167&quot; data-ke-size=&quot;size16&quot;&gt;이 설정은 Nginx가 모든 설정을 한 파일에 고정해 두는 것이 아니라,&lt;br /&gt;active.conf라는 파일을 동적으로 포함(include) 하도록 만들어줍니다.&lt;br /&gt;즉, active.conf가 어떤 파일을 가리키느냐에 따라 Blue 또는 Green으로 트래픽을 전환할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;288&quot; data-start=&quot;167&quot; data-ke-size=&quot;size16&quot;&gt;(참고로 모든 설정은 모두 같습니다.)&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;288&quot; data-start=&quot;167&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 관계상 Blue-Green 배포는 끝까지 테스트를 진행하지 못했습니다. 어쩌면 그 결과는 블로그에 모두 담지 못할 수도 있을 것 같습니다.&lt;br /&gt;하지만 Rolling과 Blue-Green 배포를 직접 구성해보며 한 가지를 분명히 느꼈습니다.&lt;br /&gt;무중단 배포는 막연히 어렵고 복잡한 기술이라고 생각했지만, 핵심 원리를 이해하고 나니 생각보다 단순한 구조라는 점이었습니다.&lt;/p&gt;
&lt;p data-end=&quot;310&quot; data-start=&quot;273&quot; data-ke-size=&quot;size16&quot;&gt;결국 Rolling이든, Blue-Green이든 본질은 동일합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;353&quot; data-start=&quot;312&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;326&quot; data-start=&quot;312&quot;&gt;인스턴스를 준비하고&lt;/li&gt;
&lt;li data-end=&quot;340&quot; data-start=&quot;327&quot;&gt;트래픽을 제어하고&lt;/li&gt;
&lt;li data-end=&quot;353&quot; data-start=&quot;341&quot;&gt;전환을 설계하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;394&quot; data-start=&quot;355&quot; data-ke-size=&quot;size16&quot;&gt;이 세 가지를 이해하면, 배포 전략은 더 이상 두려운 개념이 아닙니다.&lt;br /&gt;이번 실습을 통해 무중단 배포는 특정 도구의 기능이 아니라, 설계와 트래픽 제어의 문제라는 것을 깨닫게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/538</guid>
      <comments>https://b-programmer.tistory.com/538#entry538comment</comments>
      <pubDate>Thu, 26 Feb 2026 23:43:35 +0900</pubDate>
    </item>
    <item>
      <title>도커 네트워크 이해해보기</title>
      <link>https://b-programmer.tistory.com/537</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;도커를 학습하면서 가장 어렵게 느껴졌던 부분이 무엇이냐고 묻는다면, 저는 단연 네트워크라고 말할 것 같습니다.&lt;/p&gt;
&lt;p data-end=&quot;264&quot; data-start=&quot;115&quot; data-ke-size=&quot;size16&quot;&gt;Dockerfile은 컨테이너가 실행될 환경을 정의하는 파일이고, Docker Compose는 여러 개의 컨테이너를 하나의 서비스처럼 구성해주는 도구입니다. Volume은 컨테이너의 데이터를 영속적으로 저장하기 위해 호스트와 연결되는 저장 공간으로 이해하고 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;349&quot; data-start=&quot;266&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 각 요소의 역할은 비교적 명확하게 정리할 수 있었지만, 도커 네트워크는 개념을 아는 것과 실제로 이해하는 것 사이에 간극이 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;383&quot; data-start=&quot;351&quot; data-ke-size=&quot;size16&quot;&gt;'도커 안에 네트워크를 만든다'는 것은 알고 있었지만,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;480&quot; data-start=&quot;384&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;407&quot; data-start=&quot;384&quot;&gt;왜 굳이 네트워크를 분리해야 하는지&lt;/li&gt;
&lt;li data-end=&quot;452&quot; data-start=&quot;408&quot;&gt;언제 기본 bridge를 쓰고, 언제 사용자 정의 네트워크를 써야 하는지&lt;/li&gt;
&lt;li data-end=&quot;480&quot; data-start=&quot;453&quot;&gt;컨테이너들은 어떻게 서로를 찾고 통신하는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;501&quot; data-start=&quot;482&quot; data-ke-size=&quot;size16&quot;&gt;이 부분들은 쉽게 와닿지 않았습니다.&lt;/p&gt;
&lt;p data-end=&quot;584&quot; data-start=&quot;503&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번 글에서는 단순히 &quot;네트워크를 만든다&quot;는 수준이 아니라, 도커 네트워크를 실제로 어떻게 사용해야 하는지를 정리해보려 합니다.&lt;/p&gt;
&lt;h2 data-end=&quot;584&quot; data-start=&quot;503&quot; data-ke-size=&quot;size26&quot;&gt;컨테이너들은 어떻게 서로를 찾고 통신하는지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너가 서로 통신하기 위해서는 동일한 Docker 네트워크에 연결되어 있어야 합니다.&lt;br /&gt;각 컨테이너는 기본적으로 격리된 환경에서 실행되기 때문에, 같은 네트워크에 속해 있지 않다면 서로를 찾을 수 없습니다.&lt;/p&gt;
&lt;p data-end=&quot;334&quot; data-start=&quot;217&quot; data-ke-size=&quot;size16&quot;&gt;Docker는 하나의 네트워크만 사용하는 것이 아니라, 여러 개의 네트워크를 생성하고 관리할 수 있습니다. 이를 통해 서비스 단위로 네트워크를 분리하거나, 특정 컨테이너들만 서로 통신하도록 구성할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;407&quot; data-start=&quot;336&quot; data-ke-size=&quot;size16&quot;&gt;같은 네트워크에 속한 컨테이너들은 Docker 내부 DNS를 통해 컨테이너 이름을 기반으로 서로를 찾고 통신하게 됩니다.&lt;br /&gt;대략적으로 그리면 다음과 같이 그릴 수 있죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;1046&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1ANAc/dJMcabiZSJz/2Il1TZ7dMudez9LozvIHvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1ANAc/dJMcabiZSJz/2Il1TZ7dMudez9LozvIHvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1ANAc/dJMcabiZSJz/2Il1TZ7dMudez9LozvIHvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1ANAc%2FdJMcabiZSJz%2F2Il1TZ7dMudez9LozvIHvK%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;424&quot; height=&quot;418&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;1046&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker는 설치 시 기본 네트워크(bridge, host, none)를 자동으로 생성합니다.&lt;br /&gt;하지만 서비스 간 네트워크를 분리하거나, 이름 기반 통신을 사용하기 위해서는 사용자 정의 네트워크를 직접 생성해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;만약에, 네트워크를 설정하지 않는다면, 컨테이너는 서로 정보를 공유 할 수 있을까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 네트워크를 설정하지 않으면 통신할 수 없다고 생각할 수 있습니다. 하지만 실제로는 그렇지 않습니다. Docker는 컨테이너를 실행할 때 별도의 설정이 없더라도 기본 bridge 네트워크에 자동으로 연결합니다. 따라서 완전히 고립되는 것은 아니며, 같은 bridge 네트워크에 속한 컨테이너끼리는 통신이 가능합니다.&lt;/p&gt;
&lt;p data-end=&quot;514&quot; data-start=&quot;310&quot; data-ke-size=&quot;size16&quot;&gt;다만 여기서 중요한 차이가 있습니다. 우리가 실무에서 말하는 &quot;통신&quot;은 단순히 IP로 접근하는 것이 아니라, 컨테이너 이름을 기반으로 한 통신을 의미하는 경우가 많습니다. 사용자 정의 네트워크에서는 Docker 내부 DNS가 동작하여 컨테이너 이름으로 서로를 찾을 수 있습니다. 반면 기본 bridge 네트워크에서는 이러한 이름 기반 통신이 제한적이거나 불편합니다.&lt;/p&gt;
&lt;p data-end=&quot;616&quot; data-start=&quot;516&quot; data-ke-size=&quot;size16&quot;&gt;즉, 기본 bridge 네트워크에서도 IP를 통해 통신은 가능하지만, 이름 기반 통신이 자연스럽게 지원되지 않기 때문에 실무에서는 사용자 정의 네트워크를 사용하는 경우가 많습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면, bridge 네트워크는 어째서 DNS를 제공하지 않을까요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 Docker는 단순히 컨테이너를 실행하고 포트를 매핑하는 데 초점이 맞춰져 있었습니다. 이후 사용자 정의 네트워크와 내부 DNS 기능이 도입되었지만, 기존 환경과의 호환성을 위해 기본 bridge 네트워크의 구조는 그대로 유지되었습니다. 따라서 이름 기반 DNS는 기본 bridge가 아닌, 사용자 정의 네트워크에서 제공됩니다.&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;br /&gt;컨테이너를 실행하고, 이미지를 빌드하고, Compose로 묶는 것까지는 비교적 명확했지만, 네트워크는 어딘가 추상적으로 느껴졌습니다. 그래서 언젠가는 한 번 정리해봐야겠다고 생각했고, 이번에 차근차근 개념을 짚어보게 되었습니다.&lt;/p&gt;
&lt;p data-end=&quot;396&quot; data-start=&quot;249&quot; data-ke-size=&quot;size16&quot;&gt;막상 정리해보니 생각보다 복잡하지는 않았습니다.&lt;br /&gt;같은 네트워크에 있어야 통신할 수 있다는 점, 사용자 정의 네트워크에서 이름 기반 DNS가 동작한다는 점, 그리고 Compose가 네트워크를 어떻게 관리하는지까지 이해하고 나니 전체 구조가 비교적 명확해졌습니다.&lt;/p&gt;
&lt;p data-end=&quot;536&quot; data-start=&quot;398&quot; data-ke-size=&quot;size16&quot;&gt;물론 여기서 더 깊게 들어가면 리눅스 네트워크 네임스페이스, 브리지, iptables 같은 영역까지 확장될 수 있겠지만, 적어도 Docker를 사용하는 개발자 관점에서는 이 정도 이해만으로도 충분히 네트워크를 설계하고 활용할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-end=&quot;597&quot; data-start=&quot;538&quot; data-ke-size=&quot;size16&quot;&gt;내일부터는 무중단 배포 실습을 진행해볼 예정인데, 오늘 정리한 내용이 분명 도움이 될 것이라 기대해봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/537</guid>
      <comments>https://b-programmer.tistory.com/537#entry537comment</comments>
      <pubDate>Wed, 25 Feb 2026 21:43:40 +0900</pubDate>
    </item>
    <item>
      <title>502 게이트웨이를 없애보자. (이론편)</title>
      <link>https://b-programmer.tistory.com/536</link>
      <description>&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;2248&quot; data-origin-height=&quot;1678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dg2MCT/dJMcadufqY5/sJPM0o6FMLRiCZsGnXtje1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dg2MCT/dJMcadufqY5/sJPM0o6FMLRiCZsGnXtje1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dg2MCT/dJMcadufqY5/sJPM0o6FMLRiCZsGnXtje1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdg2MCT%2FdJMcadufqY5%2FsJPM0o6FMLRiCZsGnXtje1%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;437&quot; height=&quot;326&quot; data-origin-width=&quot;2248&quot; data-origin-height=&quot;1678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 화면은 배포 전 상태이며, localhost인 이유는 로컬 환경에서 배포를 진행했기 때문입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2928&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9bP0w/dJMcafr674Y/JJPxFdjwAJ28J27snvk00k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9bP0w/dJMcafr674Y/JJPxFdjwAJ28J27snvk00k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9bP0w/dJMcafr674Y/JJPxFdjwAJ28J27snvk00k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9bP0w%2FdJMcafr674Y%2FJJPxFdjwAJ28J27snvk00k%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;667&quot; height=&quot;139&quot; data-origin-width=&quot;2928&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 배포가 완료되는 과정에서 잠시 동안 502 Bad Gateway 오류가 발생한다는 점입니다.&lt;br /&gt;이는 프록시 서버가 백엔드 서버와 원활하게 통신하지 못하고 있음을 의미합니다.&lt;br /&gt;일정 시간이 지나야 정상적으로 서비스가 동작하며, 새로고침을 해야 변경된 화면을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;407&quot; data-start=&quot;320&quot; data-ke-size=&quot;size16&quot;&gt;지금 보고 있는 화면은 Swagger이기 때문에 크게 문제가 없어 보일 수 있습니다.&lt;br /&gt;하지만 이 상황을 실제 운영 중인 사용자 UI라고 가정해보겠습니다.&lt;/p&gt;
&lt;p data-end=&quot;578&quot; data-start=&quot;409&quot; data-ke-size=&quot;size16&quot;&gt;수백, 수천 명이 동시에 서비스를 이용하는 상황에서 배포가 이루어지고, 그 순간마다 502 오류가 발생한다면 어떻게 될까요?&lt;br /&gt;상품을 구매하려던 사용자는 갑자기 튕기고, 웹을 탐색하던 사용자도 오류 화면을 마주하게 됩니다. &lt;br /&gt;이러한 경험이 반복된다면 서비스에 대한 신뢰도는 크게 떨어질 수밖에 없습니다.&lt;/p&gt;
&lt;p data-end=&quot;690&quot; data-start=&quot;580&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 배포 과정에서 발생하는 일시적인 중단은 사용자 경험을 해칠 수 있습니다.&lt;br /&gt;이를 해결하기 위해 사용하는 방법이 바로 무중단 배포(Zero-Downtime Deployment)입니다.&lt;br /&gt;무중단 배포란, 배포가 진행되는 동안에도 서비스가 끊기지 않도록 하여 사용자가 중단을 인지하지 못하게 만드는 방식입니다.&lt;br /&gt;그렇다면, 어떻게 하면 배포 중에도 서비스가 끊기지 않도록 만들 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;무중단 배포를 하는 이유와 무중단 배포가 무엇인지는 알아봤으니&lt;br /&gt;어떻게 하면 무중단 배포를 적용할 수 있는지 확인해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.62em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;무중단 배포는 어떻게 제공할 수 있을까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무중단 배포의 핵심은 단순합니다.&lt;br /&gt;배포 중에도 트래픽을 받을 수 있는 인스턴스를 항상 최소 1개 이상 유지하는 것입니다.&lt;br /&gt;즉, 새 버전을 올리는 순간에 기존 서버가 내려가면서 생기는 &quot;빈 구간&quot;을 없애야 하고, 이를 위해 보통 서버를 2개 이상 운영하며 트래픽을 스위칭하는 방식을 사용합니다. 대표적으로 Rolling 배포, Blue-Green 배포, Canary 배포가 존재합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Rolling 배포&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스를 하나씩 교체하며 점진적으로 업데이트하는 방식&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;여기서 인스턴스를 하나씩 교체한다는 말은 다음과 같이 표현할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1610&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TSjZk/dJMb996wgV9/jwIjFqg5HkGK8bAeiJciFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TSjZk/dJMb996wgV9/jwIjFqg5HkGK8bAeiJciFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TSjZk/dJMb996wgV9/jwIjFqg5HkGK8bAeiJciFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTSjZk%2FdJMb996wgV9%2FjwIjFqg5HkGK8bAeiJciFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;257&quot; data-origin-width=&quot;1610&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 배포 과정 내내 항상 하나 이상의 인스턴스가 Up 상태를 유지하도록 만드는 것이 롤링 배포의 핵심입니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;롤링 배포는 여기까지가 전부입니다. &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그런데 여기서 두 가지 한계점이 존재합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 단일 도메인 뒤에서 이루어지는 트래픽 분산&lt;/b&gt;&lt;br /&gt;브라우저에서 URL을 입력하면 항상 같은 도메인으로 접속합니다.&lt;br /&gt;서버 IP가 변경되더라도, 사용자가 접속하는 도메인 자체가 바뀌는 일은 없습니다.&lt;br /&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하지만 실제로는 하나의 서버가 아니라, 로드밸런서(Nginx 등)를 통해 여러 인스턴스 중 하나로 요청이 전달됩니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, 사용자는 하나의 서버에 접속한다고 인식하지만, &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;실제로는 여러 인스턴스가 하나의 도메인 뒤에서 동작하고 있는 구조입니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;b&gt;2. 모든 서버(인스턴스)를 전부 띄워서 유지를 시켜야 할까?&lt;br /&gt;&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 질문에 대한 답은 뒤에서 이어서 다루겠습니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;사실 이런 배포는 쿠버네티스를 활용하면 훨씬 편하게 진행할 수 있습니다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;kubectl rollout 같은 기능으로 배포 과정을 자동화할 수 있고, 쿠버네티스의 기본 배포 전략 자체가 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Rolling Update&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;186&quot; data-start=&quot;136&quot; data-ke-size=&quot;size16&quot;&gt;하지만 현재 환경에서는 쿠버네티스를 사용하고 있지 않습니다. 그렇다면 방법이 없을까요?&lt;/p&gt;
&lt;p data-end=&quot;277&quot; data-start=&quot;188&quot; data-ke-size=&quot;size16&quot;&gt;결론부터 말하면, 방법은 있습니다.&lt;br /&gt;바로 Nginx를 이용해 트래픽을 제어하고, 그 과정을 Jenkins 파이프라인으로 자동화하는 방식입니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;375&quot; data-start=&quot;279&quot; data-ke-size=&quot;size16&quot;&gt;즉, 쿠버네티스가 해주던 인스턴스 제외 &amp;rarr; 교체 &amp;rarr; 헬스체크 &amp;rarr; 복귀 과정을&lt;br /&gt;Jenkins + Nginx 스크립트(혹은 설정 전환)로 직접 구현하는 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;89&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;이 배포 방법에는 한 가지 문제가 존재합니다.&lt;br /&gt;롤링 배포는 배포 과정에서 new 버전과 old 버전이 동시에 올라가 있는 구간이 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;265&quot; data-start=&quot;91&quot; data-ke-size=&quot;size16&quot;&gt;이때 old 버전에서는 문제가 없던 기능이 new 버전에서 버그가 발생하면, 사용자는 서비스를 정상적으로 사용하다가도 갑자기 특정 기능이 막히는 경험을 하게 됩니다.&lt;br /&gt;즉, 사용자 경험을 개선하기 위해 도입한 무중단 배포가 역설적으로 일관성 측면에서 사용자 경험을 떨어뜨릴 수 있는 가능성도 존재합니다.&lt;/p&gt;
&lt;p data-end=&quot;425&quot; data-start=&quot;267&quot; data-ke-size=&quot;size16&quot;&gt;이제 두 번째 한계점에 대해 말씀드리겠습니다.&lt;br /&gt;무중단 배포를 위해서 반드시 두 개의 인스턴스가 항상 동시에 떠 있어야 하는 것은 아닙니다.&lt;br /&gt;다만, &quot;항상 하나 이상이 살아있게 유지한다&quot;는 특성은 롤링 배포의 구현 방식에서 비롯된 특징으로 이해하시면 좋습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;480&quot; data-start=&quot;427&quot; data-ke-size=&quot;size16&quot;&gt;이러한 한계를 보완하기 위해 사용할 수 있는 방식이 바로 Blue-Green 배포입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Blue-Green 배포&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blue-Green 배포는 기존 환경(Blue)과 신규 환경(Green)을 동시에 준비한 뒤,&lt;br /&gt;검증이 완료되면 트래픽을 한 번에 전환하는 방식입니다.&lt;/p&gt;
&lt;p data-end=&quot;290&quot; data-start=&quot;186&quot; data-ke-size=&quot;size16&quot;&gt;Rolling 배포는 가용성을 유지한다는 점에서 훌륭한 전략입니다.&lt;br /&gt;하지만 배포 과정에서 old 버전과 new 버전이 동시에 존재하는 구간이 발생할 수 있다는 한계가 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;336&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;이러한 한계를 보완하기 위한 방식이 바로 Blue-Green 배포입니다.&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;1596&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYQp8e/dJMcabDgZLg/YXxr9PktSHRR7YFCVgm1XK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYQp8e/dJMcabDgZLg/YXxr9PktSHRR7YFCVgm1XK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYQp8e/dJMcabDgZLg/YXxr9PktSHRR7YFCVgm1XK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYQp8e%2FdJMcabDgZLg%2FYXxr9PktSHRR7YFCVgm1XK%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;589&quot; height=&quot;282&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;764&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은, 버전이 섞이지 않는다는 것입니다. 전환 전에는 Blue만, 전환 후에는 Green만 사용자에게 노출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 방식에도 단점은 존재합니다.&lt;br /&gt;트래픽을 다른 환경으로 전환하는 순간, 요청이 새로운 서버로 연결되는 과정에서 아주 짧은 딜레이가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;287&quot; data-start=&quot;175&quot; data-ke-size=&quot;size16&quot;&gt;하지만 전환 시점을 헬스체크와 워밍업을 통해 충분히 검증하고, 트래픽 전환을 안전하게 설계해두면 이러한 지연은 대부분 사용자가 체감하지 못하는 수준으로 충분히 커버할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블루 그린 방식은 버전이 섞이지 않는다는 장점이 있습니다.&lt;br /&gt;하지만 전환 순간, 전체 트래픽이 한 번에 새로운 버전(Green)으로 이동하게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;239&quot; data-start=&quot;162&quot; data-ke-size=&quot;size16&quot;&gt;만약 그린 환경에 치명적인 버그가 존재한다면, 그 영향은 일부 사용자가 아니라 전체 사용자에게 동시에 전달될 수 있습니다.&lt;br /&gt;즉, 성공하면 매우 안정적이고 깔끔한 배포 전략이지만, 실패할 경우에는 한 번에 큰 장애로 이어질 수 있는 구조입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Canary 배포&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 트래픽만 신규 버전에 먼저 흘려보내며 문제 여부를 확인한 뒤 점진적으로 확대하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rolling 배포는 배포 과정에서 버전 불일치가 발생할 수 있다는 한계가 있고,&lt;br /&gt;Blue-Green 배포는 전환 순간에 문제가 발생할 경우 전체 사용자에게 영향을 줄 수 있다는 위험성을 가지고 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;252&quot; data-start=&quot;208&quot; data-ke-size=&quot;size16&quot;&gt;이러한 단점을 보완하기 위해 등장한 전략이 바로 Canary 배포입니다.&lt;/p&gt;
&lt;p data-end=&quot;369&quot; data-start=&quot;254&quot; data-ke-size=&quot;size16&quot;&gt;Canary라는 이름은 과거 광부들이 유독가스를 감지하기 위해 사용했던 작은 노란 새, 카나리에서 유래했습니다.&lt;br /&gt;카나리는 유독가스에 민감하게 반응하기 때문에, 위험을 조기에 감지하는 역할을 했습니다.&lt;/p&gt;
&lt;p data-end=&quot;397&quot; data-start=&quot;371&quot; data-ke-size=&quot;size16&quot;&gt;배포 전략에서도 이 개념을 동일하게 적용합니다.&lt;/p&gt;
&lt;p data-end=&quot;480&quot; data-start=&quot;399&quot; data-ke-size=&quot;size16&quot;&gt;새로운 버전을 전체 사용자에게 한 번에 공개하는 것이 아니라,&lt;br /&gt;일부 사용자(예: 5% 또는 특정 그룹)에게 먼저 배포하여 안정성을 검증합니다.&lt;/p&gt;
&lt;p data-end=&quot;554&quot; data-start=&quot;482&quot; data-ke-size=&quot;size16&quot;&gt;이후 모니터링과 피드백을 통해 문제가 없다고 판단되면, 점진적으로 배포 범위를 확대하여 최종적으로 전체 사용자에게 적용합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 배포 전략은 서로 배타적인 선택지라기보다는, 상황에 따라 조합하여 사용할 수 있는 전략의 집합에 가깝다고 생각합니다.&lt;br /&gt;하나의 방법을 선택했다고 해서, 다른 방법을 전혀 사용하지 않는 것은 아닙니다.&lt;/p&gt;
&lt;p data-end=&quot;225&quot; data-start=&quot;219&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;365&quot; data-start=&quot;227&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;268&quot; data-start=&quot;227&quot;&gt;Rolling + Blue-Green을 함께 사용할 수도 있고,&lt;/li&gt;
&lt;li data-end=&quot;307&quot; data-start=&quot;269&quot;&gt;Rolling + Canary 전략을 적용할 수도 있으며,&lt;/li&gt;
&lt;li data-end=&quot;365&quot; data-start=&quot;308&quot;&gt;더 나아가 Rolling + Blue-Green + Canary를 조합하는 것도 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;463&quot; data-start=&quot;367&quot; data-ke-size=&quot;size16&quot;&gt;결국 배포 전략은 정답이 있는 문제가 아니라, 서비스의 규모, 리스크 허용 범위, 운영 환경에 따라&lt;br /&gt;적절히 설계해야 하는 아키텍처 선택의 문제라고 생각합니다.&lt;/p&gt;</description>
      <category>개발</category>
      <author>klom</author>
      <guid isPermaLink="true">https://b-programmer.tistory.com/536</guid>
      <comments>https://b-programmer.tistory.com/536#entry536comment</comments>
      <pubDate>Tue, 24 Feb 2026 22:42:25 +0900</pubDate>
    </item>
  </channel>
</rss>